Changeset View
Changeset View
Standalone View
Standalone View
src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java
Show All 14 Lines | |||||
15 | * GNU General Public License for more details. | 15 | * GNU General Public License for more details. | ||
16 | * | 16 | * | ||
17 | * You should have received a copy of the GNU General Public License | 17 | * You should have received a copy of the GNU General Public License | ||
18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
19 | */ | 19 | */ | ||
20 | 20 | | |||
21 | package org.kde.kdeconnect.Plugins.SftpPlugin; | 21 | package org.kde.kdeconnect.Plugins.SftpPlugin; | ||
22 | 22 | | |||
23 | import android.Manifest; | 23 | import android.content.ContentResolver; | ||
24 | import android.content.SharedPreferences; | ||||
25 | import android.net.Uri; | ||||
24 | import android.os.Build; | 26 | import android.os.Build; | ||
25 | import android.os.Environment; | | |||
26 | 27 | | |||
27 | import org.kde.kdeconnect.Helpers.StorageHelper; | 28 | import org.json.JSONException; | ||
29 | import org.json.JSONObject; | ||||
28 | import org.kde.kdeconnect.NetworkPacket; | 30 | import org.kde.kdeconnect.NetworkPacket; | ||
29 | import org.kde.kdeconnect.Plugins.Plugin; | 31 | import org.kde.kdeconnect.Plugins.Plugin; | ||
32 | import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; | ||||
30 | import org.kde.kdeconnect_tp.R; | 33 | import org.kde.kdeconnect_tp.R; | ||
31 | 34 | | |||
32 | import java.io.File; | 35 | import java.io.File; | ||
33 | import java.util.ArrayList; | 36 | import java.util.ArrayList; | ||
37 | import java.util.Collections; | ||||
38 | import java.util.Iterator; | ||||
34 | import java.util.List; | 39 | import java.util.List; | ||
35 | 40 | | |||
36 | public class SftpPlugin extends Plugin { | 41 | import androidx.annotation.NonNull; | ||
42 | import androidx.preference.PreferenceManager; | ||||
43 | | ||||
44 | public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPreferenceChangeListener { | ||||
37 | 45 | | |||
38 | private final static String PACKET_TYPE_SFTP = "kdeconnect.sftp"; | 46 | private final static String PACKET_TYPE_SFTP = "kdeconnect.sftp"; | ||
39 | private final static String PACKET_TYPE_SFTP_REQUEST = "kdeconnect.sftp.request"; | 47 | private final static String PACKET_TYPE_SFTP_REQUEST = "kdeconnect.sftp.request"; | ||
40 | 48 | | |||
41 | private static final SimpleSftpServer server = new SimpleSftpServer(); | 49 | private static final SimpleSftpServer server = new SimpleSftpServer(); | ||
42 | 50 | | |||
51 | private String KeyStorageInfoList; | ||||
52 | private String KeyAddCameraShortcut; | ||||
53 | | ||||
43 | @Override | 54 | @Override | ||
44 | public String getDisplayName() { | 55 | public String getDisplayName() { | ||
45 | return context.getResources().getString(R.string.pref_plugin_sftp); | 56 | return context.getResources().getString(R.string.pref_plugin_sftp); | ||
46 | } | 57 | } | ||
47 | 58 | | |||
48 | @Override | 59 | @Override | ||
49 | public String getDescription() { | 60 | public String getDescription() { | ||
50 | return context.getResources().getString(R.string.pref_plugin_sftp_desc); | 61 | return context.getResources().getString(R.string.pref_plugin_sftp_desc); | ||
51 | } | 62 | } | ||
52 | 63 | | |||
53 | @Override | 64 | @Override | ||
54 | public boolean onCreate() { | 65 | public boolean onCreate() { | ||
55 | permissionExplanation = R.string.sftp_permission_explanation; | 66 | KeyStorageInfoList = context.getString(R.string.sftp_preference_key_storage_info_list); | ||
67 | KeyAddCameraShortcut = context.getString(R.string.sftp_preference_key_add_camera_shortcut); | ||||
68 | | ||||
56 | try { | 69 | try { | ||
57 | server.init(context, device); | 70 | server.init(context, device); | ||
58 | return true; | 71 | return true; | ||
59 | } catch (Exception e) { | 72 | } catch (Exception e) { | ||
60 | e.printStackTrace(); | 73 | e.printStackTrace(); | ||
61 | return false; | 74 | return false; | ||
62 | } | 75 | } | ||
63 | } | 76 | } | ||
64 | 77 | | |||
65 | @Override | 78 | @Override | ||
66 | public void onDestroy() { | 79 | public void onDestroy() { | ||
67 | server.stop(); | 80 | server.stop(); | ||
81 | PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this); | ||||
68 | } | 82 | } | ||
69 | 83 | | |||
70 | @Override | 84 | @Override | ||
71 | public boolean onPacketReceived(NetworkPacket np) { | 85 | public boolean onPacketReceived(NetworkPacket np) { | ||
72 | | ||||
73 | if (np.getBoolean("startBrowsing")) { | 86 | if (np.getBoolean("startBrowsing")) { | ||
74 | if (server.start()) { | 87 | ArrayList<String> paths = new ArrayList<>(); | ||
88 | ArrayList<String> pathNames = new ArrayList<>(); | ||||
89 | | ||||
90 | List<StorageInfo> storageInfoList = SftpSettingsFragment.getStorageInfoList(context); | ||||
91 | Collections.sort(storageInfoList, new StorageInfo.UriNameComparator()); | ||||
92 | | ||||
93 | if (storageInfoList.size() > 0) { | ||||
94 | getPathsAndNamesForStorageInfoList(paths, pathNames, storageInfoList); | ||||
95 | } else { | ||||
96 | paths.add("/"); | ||||
97 | pathNames.add(context.getString(R.string.sftp_all_files)); | ||||
98 | | ||||
99 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { | ||||
100 | storageInfoList.add(new StorageInfo("/", Uri.parse("file:///"))); | ||||
101 | } | ||||
102 | } | ||||
103 | | ||||
104 | removeChildren(storageInfoList); | ||||
105 | | ||||
106 | if (server.start(storageInfoList)) { | ||||
107 | PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this); | ||||
75 | 108 | | |||
76 | NetworkPacket np2 = new NetworkPacket(PACKET_TYPE_SFTP); | 109 | NetworkPacket np2 = new NetworkPacket(PACKET_TYPE_SFTP); | ||
77 | 110 | | |||
111 | //TODO: ip is not used on desktop any more remove both here and from desktop code when nobody ships 1.2.0 anymore | ||||
78 | np2.set("ip", server.getLocalIpAddress()); | 112 | np2.set("ip", server.getLocalIpAddress()); | ||
79 | np2.set("port", server.getPort()); | 113 | np2.set("port", server.getPort()); | ||
80 | np2.set("user", SimpleSftpServer.USER); | 114 | np2.set("user", SimpleSftpServer.USER); | ||
81 | np2.set("password", server.getPassword()); | 115 | np2.set("password", server.getPassword()); | ||
82 | 116 | | |||
83 | //Kept for compatibility, in case "multiPaths" is not possible or the other end does not support it | 117 | //Kept for compatibility, in case "multiPaths" is not possible or the other end does not support it | ||
84 | np2.set("path", Environment.getExternalStorageDirectory().getAbsolutePath()); | 118 | np2.set("path", "/"); | ||
85 | 119 | | |||
86 | List<StorageHelper.StorageInfo> storageList = StorageHelper.getStorageList(); | 120 | if (paths.size() > 0) { | ||
87 | ArrayList<String> paths = new ArrayList<>(); | 121 | np2.set("multiPaths", paths); | ||
88 | ArrayList<String> pathNames = new ArrayList<>(); | 122 | np2.set("pathNames", pathNames); | ||
123 | } | ||||
124 | | ||||
125 | device.sendPacket(np2); | ||||
126 | | ||||
127 | return true; | ||||
128 | } | ||||
129 | } | ||||
130 | return false; | ||||
131 | } | ||||
89 | 132 | | |||
90 | for (StorageHelper.StorageInfo storage : storageList) { | 133 | private void getPathsAndNamesForStorageInfoList(List<String> paths, List<String> pathNames, List<StorageInfo> storageInfoList) { | ||
91 | paths.add(storage.path); | 134 | StorageInfo prevInfo = null; | ||
92 | StringBuilder res = new StringBuilder(); | 135 | StringBuilder pathBuilder = new StringBuilder(); | ||
93 | 136 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); | |||
94 | if (storageList.size() > 1) { | 137 | boolean addCameraShortcuts = false; | ||
95 | if (!storage.removable) { | 138 | | ||
96 | res.append(context.getString(R.string.sftp_internal_storage)); | 139 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { | ||
97 | } else if (storage.number > 1) { | 140 | addCameraShortcuts = prefs.getBoolean(context.getString(R.string.sftp_preference_key_add_camera_shortcut), false); | ||
albertvaka: This default should be `true` to match the default state of the setting. I had to disable and… | |||||
98 | res.append(context.getString(R.string.sftp_sdcard_num, storage.number)); | 141 | } | ||
142 | | ||||
143 | for (StorageInfo curInfo : storageInfoList) { | ||||
144 | pathBuilder.setLength(0); | ||||
145 | pathBuilder.append("/"); | ||||
146 | | ||||
147 | if (prevInfo != null && curInfo.uri.toString().startsWith(prevInfo.uri.toString())) { | ||||
148 | pathBuilder.append(prevInfo.displayName); | ||||
149 | pathBuilder.append("/"); | ||||
150 | if (curInfo.uri.getPath() != null && prevInfo.uri.getPath() != null) { | ||||
151 | pathBuilder.append(curInfo.uri.getPath().substring(prevInfo.uri.getPath().length())); | ||||
99 | } else { | 152 | } else { | ||
100 | res.append(context.getString(R.string.sftp_sdcard)); | 153 | throw new RuntimeException("curInfo.uri.getPath() or parentInfo.uri.getPath() returned null"); | ||
101 | } | 154 | } | ||
102 | } else { | 155 | } else { | ||
103 | res.append(context.getString(R.string.sftp_all_files)); | 156 | pathBuilder.append(curInfo.displayName); | ||
157 | | ||||
158 | if (prevInfo == null || !curInfo.uri.toString().startsWith(prevInfo.uri.toString())) { | ||||
159 | prevInfo = curInfo; | ||||
104 | } | 160 | } | ||
105 | String pathName = res.toString(); | 161 | } | ||
106 | if (storage.readonly) { | 162 | | ||
107 | res.append(" "); | 163 | paths.add(pathBuilder.toString()); | ||
108 | res.append(context.getString(R.string.sftp_readonly)); | 164 | pathNames.add(curInfo.displayName); | ||
109 | } | 165 | | ||
110 | pathNames.add(res.toString()); | 166 | if (addCameraShortcuts) { | ||
111 | 167 | if (new File(curInfo.uri.getPath(), "/DCIM/Camera").exists()) { | |||
112 | //Shortcut for users that only want to browse camera pictures | 168 | paths.add(pathBuilder.toString() + "/DCIM/Camera"); | ||
113 | String dcim = storage.path + "/DCIM/Camera"; | 169 | if (storageInfoList.size() > 1) { | ||
114 | if (new File(dcim).exists()) { | 170 | pathNames.add(context.getString(R.string.sftp_camera) + "(" + curInfo.displayName + ")"); | ||
115 | paths.add(dcim); | | |||
116 | if (storageList.size() > 1) { | | |||
117 | pathNames.add(context.getString(R.string.sftp_camera) + "(" + pathName + ")"); | | |||
118 | } else { | 171 | } else { | ||
119 | pathNames.add(context.getString(R.string.sftp_camera)); | 172 | pathNames.add(context.getString(R.string.sftp_camera)); | ||
120 | } | 173 | } | ||
121 | } | 174 | } | ||
122 | } | 175 | } | ||
176 | } | ||||
177 | } | ||||
123 | 178 | | |||
124 | if (paths.size() > 0) { | 179 | private void removeChildren(List<StorageInfo> storageInfoList) { | ||
125 | np2.set("multiPaths", paths); | 180 | StorageInfo prevInfo = null; | ||
126 | np2.set("pathNames", pathNames); | 181 | Iterator<StorageInfo> it = storageInfoList.iterator(); | ||
182 | | ||||
183 | while (it.hasNext()) { | ||||
184 | StorageInfo curInfo = it.next(); | ||||
127 | 185 | | |||
186 | if (prevInfo != null && curInfo.uri.toString().startsWith(prevInfo.uri.toString())) { | ||||
187 | it.remove(); | ||||
188 | } else { | ||||
189 | if (prevInfo == null || !curInfo.uri.toString().startsWith(prevInfo.uri.toString())) { | ||||
190 | prevInfo = curInfo; | ||||
191 | } | ||||
192 | } | ||||
193 | } | ||||
128 | } | 194 | } | ||
129 | 195 | | |||
130 | device.sendPacket(np2); | 196 | @Override | ||
197 | public String[] getSupportedPacketTypes() { | ||||
198 | return new String[]{PACKET_TYPE_SFTP_REQUEST}; | ||||
199 | } | ||||
131 | 200 | | |||
132 | return true; | 201 | @Override | ||
202 | public String[] getOutgoingPacketTypes() { | ||||
203 | return new String[]{PACKET_TYPE_SFTP}; | ||||
133 | } | 204 | } | ||
205 | | ||||
206 | @Override | ||||
207 | public boolean hasSettings() { | ||||
208 | return true; | ||||
134 | } | 209 | } | ||
135 | return false; | 210 | | ||
211 | @Override | ||||
212 | public PluginSettingsFragment getSettingsFragment() { | ||||
213 | return SftpSettingsFragment.newInstance(getPluginKey()); | ||||
136 | } | 214 | } | ||
137 | 215 | | |||
138 | @Override | 216 | @Override | ||
139 | public String[] getRequiredPermissions() { | 217 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { | ||
140 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { | 218 | if (key.equals(KeyStorageInfoList) || key.equals(KeyAddCameraShortcut)) { | ||
141 | return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; | 219 | //TODO: There used to be a way to request an un-mount (see desktop SftpPlugin's Mounter::onPackageReceived) but that is not handled anymore by the SftpPlugin on KDE. | ||
142 | } else { | 220 | if (server.isStarted()) { | ||
143 | return new String[0]; | 221 | server.stop(); | ||
222 | | ||||
223 | NetworkPacket np = new NetworkPacket(PACKET_TYPE_SFTP_REQUEST); | ||||
224 | np.set("startBrowsing", true); | ||||
225 | onPacketReceived(np); | ||||
226 | } | ||||
227 | } | ||||
228 | } | ||||
229 | | ||||
230 | static class StorageInfo { | ||||
231 | private static final String KEY_DISPLAY_NAME = "DisplayName"; | ||||
232 | private static final String KEY_URI = "Uri"; | ||||
233 | | ||||
234 | @NonNull String displayName; | ||||
235 | @NonNull Uri uri; | ||||
236 | | ||||
237 | StorageInfo(@NonNull String displayName, @NonNull Uri uri) { | ||||
238 | this.displayName = displayName; | ||||
239 | this.uri = uri; | ||||
240 | } | ||||
241 | | ||||
242 | static StorageInfo copy(StorageInfo from) { | ||||
243 | //Both String and Uri are immutable | ||||
244 | return new StorageInfo(from.displayName, from.uri); | ||||
245 | } | ||||
246 | | ||||
247 | boolean isFileUri() { | ||||
248 | return uri.getScheme().equals(ContentResolver.SCHEME_FILE); | ||||
249 | } | ||||
250 | | ||||
251 | boolean isContentUri() { | ||||
252 | return uri.getScheme().equals(ContentResolver.SCHEME_CONTENT); | ||||
253 | } | ||||
254 | | ||||
255 | public JSONObject toJSON() throws JSONException { | ||||
256 | JSONObject jsonObject = new JSONObject(); | ||||
257 | | ||||
258 | jsonObject.put(KEY_DISPLAY_NAME, displayName); | ||||
259 | jsonObject.put(KEY_URI, uri.toString()); | ||||
260 | | ||||
261 | return jsonObject; | ||||
144 | } | 262 | } | ||
263 | | ||||
264 | @NonNull | ||||
265 | static StorageInfo fromJSON(@NonNull JSONObject jsonObject) throws JSONException { | ||||
266 | String displayName = jsonObject.getString(KEY_DISPLAY_NAME); | ||||
267 | Uri uri = Uri.parse(jsonObject.getString(KEY_URI)); | ||||
268 | | ||||
269 | return new StorageInfo(displayName, uri); | ||||
145 | } | 270 | } | ||
146 | 271 | | |||
147 | @Override | 272 | @Override | ||
148 | public String[] getSupportedPacketTypes() { | 273 | public boolean equals(Object o) { | ||
149 | return new String[]{PACKET_TYPE_SFTP_REQUEST}; | 274 | if (this == o) return true; | ||
275 | if (o == null || getClass() != o.getClass()) return false; | ||||
276 | | ||||
277 | StorageInfo that = (StorageInfo) o; | ||||
278 | | ||||
279 | if (!displayName.equals(that.displayName)) return false; | ||||
280 | return uri.equals(that.uri); | ||||
150 | } | 281 | } | ||
151 | 282 | | |||
152 | @Override | 283 | @Override | ||
153 | public String[] getOutgoingPacketTypes() { | 284 | public int hashCode() { | ||
154 | return new String[]{PACKET_TYPE_SFTP}; | 285 | int result = displayName.hashCode(); | ||
286 | result = 31 * result + uri.hashCode(); | ||||
287 | return result; | ||||
155 | } | 288 | } | ||
156 | 289 | | |||
290 | static class DisplayNameComparator implements java.util.Comparator<StorageInfo> { | ||||
291 | @Override | ||||
292 | public int compare(StorageInfo si1, StorageInfo si2) { | ||||
293 | return si1.displayName.compareToIgnoreCase(si2.displayName); | ||||
294 | } | ||||
295 | } | ||||
296 | | ||||
297 | static class UriNameComparator implements java.util.Comparator<StorageInfo> { | ||||
298 | @Override | ||||
299 | public int compare(StorageInfo si1, StorageInfo si2) { | ||||
300 | return si1.uri.compareTo(si2.uri); | ||||
301 | } | ||||
302 | } | ||||
303 | } | ||||
157 | } | 304 | } |
This default should be true to match the default state of the setting. I had to disable and re-enable the setting for the Camera folder to appear.