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 }