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