From 1ba9e59872e811615a152882469e9b9f07cc7001 Mon Sep 17 00:00:00 2001 From: Albert Vaca Cintora Date: Tue, 7 Mar 2023 23:59:40 +0100 Subject: [PATCH] SftpPlugin: use MANAGE_EXTERNAL_STORAGE instead of SAF in Android 11+ https://developer.android.com/training/data-storage/manage-all-files BUG: 447636 BUG: 464431 --- AndroidManifest.xml | 1 + res/values/strings.xml | 1 + .../Plugins/SftpPlugin/SftpPlugin.java | 89 +++++++++++++------ .../Plugins/SftpPlugin/SimpleSftpServer.java | 19 ++-- .../StartActivityAlertDialogFragment.java | 22 ++++- 5 files changed, 99 insertions(+), 33 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a480d7bf3..5c9f16a88 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -47,6 +47,7 @@ + Delete No storage locations configured To access files remotely you have to configure storage locations + To allow remote access to files on this device you need to allow KDE Connect to manage the storage. No players found Send files diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java index 3a86100aa..878a4ab17 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java @@ -8,11 +8,18 @@ import android.app.Activity; import android.content.ContentResolver; +import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.storage.StorageManager; +import android.os.storage.StorageVolume; +import android.provider.Settings; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import org.json.JSONException; import org.json.JSONObject; @@ -21,9 +28,13 @@ import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.UserInterface.AlertDialogFragment; import org.kde.kdeconnect.UserInterface.DeviceSettingsAlertDialogFragment; +import org.kde.kdeconnect.UserInterface.MainActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; +import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment; +import org.kde.kdeconnect_tp.BuildConfig; import org.kde.kdeconnect_tp.R; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -63,19 +74,36 @@ public boolean onCreate() { @Override public boolean checkRequiredPermissions() { - return SftpSettingsFragment.getStorageInfoList(context, this).size() != 0; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return Environment.isExternalStorageManager(); + } else { + return SftpSettingsFragment.getStorageInfoList(context, this).size() != 0; + } } @Override public AlertDialogFragment getPermissionExplanationDialog() { - return new DeviceSettingsAlertDialogFragment.Builder() - .setTitle(getDisplayName()) - .setMessage(R.string.sftp_saf_permission_explanation) - .setPositiveButton(R.string.ok) - .setNegativeButton(R.string.cancel) - .setDeviceId(device.getDeviceId()) - .setPluginKey(getPluginKey()) - .create(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return new StartActivityAlertDialogFragment.Builder() + .setTitle(getDisplayName()) + .setMessage(R.string.sftp_manage_storage_permission_explanation) + .setPositiveButton(R.string.open_settings) + .setNegativeButton(R.string.cancel) + .setIntentAction(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + .setIntentUrl("package:" + BuildConfig.APPLICATION_ID) + .setStartForResult(true) + .setRequestCode(MainActivity.RESULT_NEEDS_RELOAD) + .create(); + } else { + return new DeviceSettingsAlertDialogFragment.Builder() + .setTitle(getDisplayName()) + .setMessage(R.string.sftp_saf_permission_explanation) + .setPositiveButton(R.string.ok) + .setNegativeButton(R.string.cancel) + .setDeviceId(device.getDeviceId()) + .setPluginKey(getPluginKey()) + .create(); + } } @Override @@ -92,35 +120,45 @@ public boolean onPacketReceived(NetworkPacket np) { ArrayList paths = new ArrayList<>(); ArrayList pathNames = new ArrayList<>(); - List storageInfoList = SftpSettingsFragment.getStorageInfoList(context, this); - Collections.sort(storageInfoList, Comparator.comparing(StorageInfo::getUri)); - - if (storageInfoList.size() > 0) { - getPathsAndNamesForStorageInfoList(paths, pathNames, storageInfoList); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + List volumes = context.getSystemService(StorageManager.class).getStorageVolumes(); + for (StorageVolume sv : volumes) { + pathNames.add(sv.getDescription(context)); + paths.add(sv.getDirectory().getPath()); + } } else { - NetworkPacket np2 = new NetworkPacket(PACKET_TYPE_SFTP); - np2.set("errorMessage", context.getString(R.string.sftp_no_storage_locations_configured)); - device.sendPacket(np2); - return true; + List storageInfoList = SftpSettingsFragment.getStorageInfoList(context, this); + Collections.sort(storageInfoList, Comparator.comparing(StorageInfo::getUri)); + if (storageInfoList.size() > 0) { + getPathsAndNamesForStorageInfoList(paths, pathNames, storageInfoList); + } else { + NetworkPacket np2 = new NetworkPacket(PACKET_TYPE_SFTP); + np2.set("errorMessage", context.getString(R.string.sftp_no_storage_locations_configured)); + device.sendPacket(np2); + return true; + } + removeChildren(storageInfoList); + server.setSafRoots(storageInfoList); } - removeChildren(storageInfoList); - - if (server.start(storageInfoList)) { + if (server.start()) { if (preferences != null) { preferences.registerOnSharedPreferenceChangeListener(this); } NetworkPacket np2 = new NetworkPacket(PACKET_TYPE_SFTP); - //TODO: ip is not used on desktop any more remove both here and from desktop code when nobody ships 1.2.0 - np2.set("ip", server.getLocalIpAddress()); + np2.set("ip", server.getLocalIpAddress()); // for backwards compatibility np2.set("port", server.getPort()); np2.set("user", SimpleSftpServer.USER); np2.set("password", server.getPassword()); //Kept for compatibility, in case "multiPaths" is not possible or the other end does not support it - np2.set("path", "/"); + if (paths.size() == 1) { + np2.set("path", paths.get(0)); + } else { + np2.set("path", "/"); + } if (paths.size() > 0) { np2.set("multiPaths", paths); @@ -193,7 +231,7 @@ public String[] getOutgoingPacketTypes() { @Override public boolean hasSettings() { - return true; + return Build.VERSION.SDK_INT < Build.VERSION_CODES.R; } @Override @@ -227,7 +265,6 @@ public PluginSettingsFragment getSettingsFragment(Activity activity) { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(context.getString(PREFERENCE_KEY_STORAGE_INFO_LIST))) { - //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. if (server.isStarted()) { server.stop(); diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java index d7ba011a8..de60bf788 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java @@ -7,9 +7,11 @@ package org.kde.kdeconnect.Plugins.SftpPlugin; import android.content.Context; +import android.os.Build; import android.util.Log; import org.apache.sshd.SshServer; +import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory; import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider; import org.apache.sshd.common.util.SecurityUtils; import org.apache.sshd.server.PasswordAuthenticator; @@ -57,7 +59,11 @@ class SimpleSftpServer { } private final SshServer sshd = SshServer.setUpDefaultServer(); - private AndroidFileSystemFactory fileSystemFactory; + private AndroidFileSystemFactory safFileSystemFactory; + + public void setSafRoots(List storageInfoList) { + safFileSystemFactory.initRoots(storageInfoList); + } void init(Context context, Device device) throws GeneralSecurityException { @@ -78,8 +84,12 @@ public Iterable loadKeys() { } }); - fileSystemFactory = new AndroidFileSystemFactory(context); - sshd.setFileSystemFactory(fileSystemFactory); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + sshd.setFileSystemFactory(new NativeFileSystemFactory()); + } else { + safFileSystemFactory = new AndroidFileSystemFactory(context); + sshd.setFileSystemFactory(safFileSystemFactory); + } sshd.setCommandFactory(new ScpCommandFactory()); sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystem.Factory())); @@ -89,9 +99,8 @@ public Iterable loadKeys() { sshd.setPasswordAuthenticator(passwordAuth); } - public boolean start(List storageInfoList) { + public boolean start() { if (!started) { - fileSystemFactory.initRoots(storageInfoList); passwordAuth.password = RandomHelper.randomString(28); port = STARTPORT; diff --git a/src/org/kde/kdeconnect/UserInterface/StartActivityAlertDialogFragment.java b/src/org/kde/kdeconnect/UserInterface/StartActivityAlertDialogFragment.java index 62a642a71..1a481a4a7 100644 --- a/src/org/kde/kdeconnect/UserInterface/StartActivityAlertDialogFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/StartActivityAlertDialogFragment.java @@ -7,17 +7,23 @@ package org.kde.kdeconnect.UserInterface; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.kde.kdeconnect_tp.BuildConfig; + public class StartActivityAlertDialogFragment extends AlertDialogFragment { private static final String KEY_INTENT_ACTION = "IntentAction"; + private static final String KEY_INTENT_URL = "IntentUrl"; private static final String KEY_REQUEST_CODE = "RequestCode"; private static final String KEY_START_FOR_RESULT = "StartForResult"; private String intentAction; + private String intentUrl; private int requestCode; private boolean startForResult; @@ -34,6 +40,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { } intentAction = args.getString(KEY_INTENT_ACTION); + intentUrl = args.getString(KEY_INTENT_URL); requestCode = args.getInt(KEY_REQUEST_CODE, 0); startForResult = args.getBoolean(KEY_START_FOR_RESULT); @@ -44,8 +51,13 @@ public void onCreate(@Nullable Bundle savedInstanceState) { setCallback(new Callback() { @Override public void onPositiveButtonClicked() { - Intent intent = new Intent(intentAction); - + Intent intent; + if (StringUtils.isNotEmpty(intentUrl)) { + Uri uri = Uri.parse(intentUrl); + intent = new Intent(intentAction, uri); + } else { + intent = new Intent(intentAction); + } if (startForResult) { requireActivity().startActivityForResult(intent, requestCode); } else { @@ -67,6 +79,12 @@ public StartActivityAlertDialogFragment.Builder setIntentAction(@NonNull String return getThis(); } + public StartActivityAlertDialogFragment.Builder setIntentUrl(@NonNull String intentUrl) { + args.putString(KEY_INTENT_URL, intentUrl); + + return getThis(); + } + public StartActivityAlertDialogFragment.Builder setRequestCode(int requestCode) { args.putInt(KEY_REQUEST_CODE, requestCode);