File indexing completed on 2024-12-22 04:41:40
0001 /* 0002 * SPDX-FileCopyrightText: 2014 Samoilenko Yuri <kinnalru@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 package org.kde.kdeconnect.Plugins.SftpPlugin; 0008 0009 import android.app.Activity; 0010 import android.content.ContentResolver; 0011 import android.content.SharedPreferences; 0012 import android.net.Uri; 0013 import android.os.Build; 0014 import android.os.Environment; 0015 import android.os.storage.StorageManager; 0016 import android.os.storage.StorageVolume; 0017 import android.provider.Settings; 0018 0019 import androidx.annotation.NonNull; 0020 0021 import org.json.JSONException; 0022 import org.json.JSONObject; 0023 import org.kde.kdeconnect.Helpers.NetworkHelper; 0024 import org.kde.kdeconnect.NetworkPacket; 0025 import org.kde.kdeconnect.Plugins.Plugin; 0026 import org.kde.kdeconnect.Plugins.PluginFactory; 0027 import org.kde.kdeconnect.UserInterface.AlertDialogFragment; 0028 import org.kde.kdeconnect.UserInterface.DeviceSettingsAlertDialogFragment; 0029 import org.kde.kdeconnect.UserInterface.MainActivity; 0030 import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; 0031 import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment; 0032 import org.kde.kdeconnect_tp.BuildConfig; 0033 import org.kde.kdeconnect_tp.R; 0034 0035 import java.security.GeneralSecurityException; 0036 import java.util.ArrayList; 0037 import java.util.Collections; 0038 import java.util.Comparator; 0039 import java.util.Iterator; 0040 import java.util.List; 0041 0042 @PluginFactory.LoadablePlugin 0043 public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPreferenceChangeListener { 0044 0045 private final static String PACKET_TYPE_SFTP = "kdeconnect.sftp"; 0046 private final static String PACKET_TYPE_SFTP_REQUEST = "kdeconnect.sftp.request"; 0047 0048 static final int PREFERENCE_KEY_STORAGE_INFO_LIST = R.string.sftp_preference_key_storage_info_list; 0049 0050 private static final SimpleSftpServer server = new SimpleSftpServer(); 0051 0052 @Override 0053 public @NonNull String getDisplayName() { 0054 return context.getResources().getString(R.string.pref_plugin_sftp); 0055 } 0056 0057 @Override 0058 public @NonNull String getDescription() { 0059 return context.getResources().getString(R.string.pref_plugin_sftp_desc); 0060 } 0061 0062 @Override 0063 public boolean onCreate() { 0064 return true; 0065 } 0066 0067 @Override 0068 public boolean checkRequiredPermissions() { 0069 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 0070 return Environment.isExternalStorageManager(); 0071 } else { 0072 return SftpSettingsFragment.getStorageInfoList(context, this).size() != 0; 0073 } 0074 } 0075 0076 @Override 0077 public @NonNull AlertDialogFragment getPermissionExplanationDialog() { 0078 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 0079 return new StartActivityAlertDialogFragment.Builder() 0080 .setTitle(getDisplayName()) 0081 .setMessage(R.string.sftp_manage_storage_permission_explanation) 0082 .setPositiveButton(R.string.open_settings) 0083 .setNegativeButton(R.string.cancel) 0084 .setIntentAction(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) 0085 .setIntentUrl("package:" + BuildConfig.APPLICATION_ID) 0086 .setStartForResult(true) 0087 .setRequestCode(MainActivity.RESULT_NEEDS_RELOAD) 0088 .create(); 0089 } else { 0090 return new DeviceSettingsAlertDialogFragment.Builder() 0091 .setTitle(getDisplayName()) 0092 .setMessage(R.string.sftp_saf_permission_explanation) 0093 .setPositiveButton(R.string.ok) 0094 .setNegativeButton(R.string.cancel) 0095 .setDeviceId(device.getDeviceId()) 0096 .setPluginKey(getPluginKey()) 0097 .create(); 0098 } 0099 } 0100 0101 @Override 0102 public void onDestroy() { 0103 server.stop(); 0104 if (preferences != null) { 0105 preferences.unregisterOnSharedPreferenceChangeListener(this); 0106 } 0107 } 0108 0109 @Override 0110 public boolean onPacketReceived(@NonNull NetworkPacket np) { 0111 if (np.getBoolean("startBrowsing")) { 0112 if (!server.isInitialized()) { 0113 try { 0114 server.initialize(context, device); 0115 } catch (GeneralSecurityException e) { 0116 throw new RuntimeException(e); 0117 } 0118 } 0119 0120 ArrayList<String> paths = new ArrayList<>(); 0121 ArrayList<String> pathNames = new ArrayList<>(); 0122 0123 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 0124 List<StorageVolume> volumes = context.getSystemService(StorageManager.class).getStorageVolumes(); 0125 for (StorageVolume sv : volumes) { 0126 pathNames.add(sv.getDescription(context)); 0127 paths.add(sv.getDirectory().getPath()); 0128 } 0129 } else { 0130 List<StorageInfo> storageInfoList = SftpSettingsFragment.getStorageInfoList(context, this); 0131 Collections.sort(storageInfoList, Comparator.comparing(StorageInfo::getUri)); 0132 if (storageInfoList.size() > 0) { 0133 getPathsAndNamesForStorageInfoList(paths, pathNames, storageInfoList); 0134 } else { 0135 NetworkPacket np2 = new NetworkPacket(PACKET_TYPE_SFTP); 0136 np2.set("errorMessage", context.getString(R.string.sftp_no_storage_locations_configured)); 0137 device.sendPacket(np2); 0138 return true; 0139 } 0140 removeChildren(storageInfoList); 0141 server.setSafRoots(storageInfoList); 0142 } 0143 0144 if (server.start()) { 0145 if (preferences != null) { 0146 preferences.registerOnSharedPreferenceChangeListener(this); 0147 } 0148 0149 NetworkPacket np2 = new NetworkPacket(PACKET_TYPE_SFTP); 0150 0151 np2.set("ip", NetworkHelper.getLocalIpAddress().getHostAddress()); 0152 np2.set("port", server.getPort()); 0153 np2.set("user", SimpleSftpServer.USER); 0154 np2.set("password", server.getPassword()); 0155 0156 //Kept for compatibility, in case "multiPaths" is not possible or the other end does not support it 0157 if (paths.size() == 1) { 0158 np2.set("path", paths.get(0)); 0159 } else { 0160 np2.set("path", "/"); 0161 } 0162 0163 if (paths.size() > 0) { 0164 np2.set("multiPaths", paths); 0165 np2.set("pathNames", pathNames); 0166 } 0167 0168 device.sendPacket(np2); 0169 0170 return true; 0171 } 0172 } 0173 return false; 0174 } 0175 0176 private void getPathsAndNamesForStorageInfoList(List<String> paths, List<String> pathNames, List<StorageInfo> storageInfoList) { 0177 StorageInfo prevInfo = null; 0178 StringBuilder pathBuilder = new StringBuilder(); 0179 0180 for (StorageInfo curInfo : storageInfoList) { 0181 pathBuilder.setLength(0); 0182 pathBuilder.append("/"); 0183 0184 if (prevInfo != null && curInfo.uri.toString().startsWith(prevInfo.uri.toString())) { 0185 pathBuilder.append(prevInfo.displayName); 0186 pathBuilder.append("/"); 0187 if (curInfo.uri.getPath() != null && prevInfo.uri.getPath() != null) { 0188 pathBuilder.append(curInfo.uri.getPath().substring(prevInfo.uri.getPath().length())); 0189 } else { 0190 throw new RuntimeException("curInfo.uri.getPath() or parentInfo.uri.getPath() returned null"); 0191 } 0192 } else { 0193 pathBuilder.append(curInfo.displayName); 0194 0195 if (prevInfo == null || !curInfo.uri.toString().startsWith(prevInfo.uri.toString())) { 0196 prevInfo = curInfo; 0197 } 0198 } 0199 0200 paths.add(pathBuilder.toString()); 0201 pathNames.add(curInfo.displayName); 0202 } 0203 } 0204 0205 private void removeChildren(List<StorageInfo> storageInfoList) { 0206 StorageInfo prevInfo = null; 0207 Iterator<StorageInfo> it = storageInfoList.iterator(); 0208 0209 while (it.hasNext()) { 0210 StorageInfo curInfo = it.next(); 0211 0212 if (prevInfo != null && curInfo.uri.toString().startsWith(prevInfo.uri.toString())) { 0213 it.remove(); 0214 } else { 0215 if (prevInfo == null || !curInfo.uri.toString().startsWith(prevInfo.uri.toString())) { 0216 prevInfo = curInfo; 0217 } 0218 } 0219 } 0220 } 0221 0222 @Override 0223 public @NonNull String[] getSupportedPacketTypes() { 0224 return new String[]{PACKET_TYPE_SFTP_REQUEST}; 0225 } 0226 0227 @Override 0228 public @NonNull String[] getOutgoingPacketTypes() { 0229 return new String[]{PACKET_TYPE_SFTP}; 0230 } 0231 0232 @Override 0233 public boolean hasSettings() { 0234 return Build.VERSION.SDK_INT < Build.VERSION_CODES.R; 0235 } 0236 0237 @Override 0238 public boolean supportsDeviceSpecificSettings() { return true; } 0239 0240 @Override 0241 public PluginSettingsFragment getSettingsFragment(Activity activity) { 0242 return SftpSettingsFragment.newInstance(getPluginKey(), R.xml.sftpplugin_preferences); 0243 } 0244 0245 @Override 0246 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 0247 if (key.equals(context.getString(PREFERENCE_KEY_STORAGE_INFO_LIST))) { 0248 if (server.isStarted()) { 0249 server.stop(); 0250 0251 NetworkPacket np = new NetworkPacket(PACKET_TYPE_SFTP_REQUEST); 0252 np.set("startBrowsing", true); 0253 onPacketReceived(np); 0254 } 0255 } 0256 } 0257 0258 static class StorageInfo { 0259 private static final String KEY_DISPLAY_NAME = "DisplayName"; 0260 private static final String KEY_URI = "Uri"; 0261 0262 @NonNull 0263 String displayName; 0264 @NonNull 0265 final Uri uri; 0266 0267 StorageInfo(@NonNull String displayName, @NonNull Uri uri) { 0268 this.displayName = displayName; 0269 this.uri = uri; 0270 } 0271 0272 @NonNull 0273 Uri getUri() { 0274 return uri; 0275 } 0276 0277 static StorageInfo copy(StorageInfo from) { 0278 //Both String and Uri are immutable 0279 return new StorageInfo(from.displayName, from.uri); 0280 } 0281 0282 boolean isFileUri() { 0283 return uri.getScheme().equals(ContentResolver.SCHEME_FILE); 0284 } 0285 0286 boolean isContentUri() { 0287 return uri.getScheme().equals(ContentResolver.SCHEME_CONTENT); 0288 } 0289 0290 public JSONObject toJSON() throws JSONException { 0291 JSONObject jsonObject = new JSONObject(); 0292 0293 jsonObject.put(KEY_DISPLAY_NAME, displayName); 0294 jsonObject.put(KEY_URI, uri.toString()); 0295 0296 return jsonObject; 0297 } 0298 0299 @NonNull 0300 static StorageInfo fromJSON(@NonNull JSONObject jsonObject) throws JSONException { 0301 String displayName = jsonObject.getString(KEY_DISPLAY_NAME); 0302 Uri uri = Uri.parse(jsonObject.getString(KEY_URI)); 0303 0304 return new StorageInfo(displayName, uri); 0305 } 0306 0307 @Override 0308 public boolean equals(Object o) { 0309 if (this == o) return true; 0310 if (o == null || getClass() != o.getClass()) return false; 0311 0312 StorageInfo that = (StorageInfo) o; 0313 0314 if (!displayName.equals(that.displayName)) return false; 0315 return uri.equals(that.uri); 0316 } 0317 0318 @Override 0319 public int hashCode() { 0320 int result = displayName.hashCode(); 0321 result = 31 * result + uri.hashCode(); 0322 return result; 0323 } 0324 } 0325 }