2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 09:58:08 +00:00

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
This commit is contained in:
Albert Vaca Cintora 2023-03-07 23:59:40 +01:00 committed by Albert Vaca Cintora
parent 76c3cc4c57
commit 1ba9e59872
5 changed files with 99 additions and 33 deletions

View File

@ -47,6 +47,7 @@
<uses-permission android:name="android.permission.READ_LOGS" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:icon="@mipmap/ic_launcher"

View File

@ -283,6 +283,7 @@
<string name="sftp_action_mode_menu_delete">Delete</string>
<string name="sftp_no_storage_locations_configured">No storage locations configured</string>
<string name="sftp_saf_permission_explanation">To access files remotely you have to configure storage locations</string>
<string name="sftp_manage_storage_permission_explanation">To allow remote access to files on this device you need to allow KDE Connect to manage the storage.</string>
<string name="no_players_connected">No players found</string>
<string name="send_files">Send files</string>

View File

@ -8,11 +8,18 @@ package org.kde.kdeconnect.Plugins.SftpPlugin;
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.Plugin;
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,11 +74,27 @@ public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPref
@Override
public boolean checkRequiredPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Environment.isExternalStorageManager();
} else {
return SftpSettingsFragment.getStorageInfoList(context, this).size() != 0;
}
}
@Override
public AlertDialogFragment getPermissionExplanationDialog() {
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)
@ -77,6 +104,7 @@ public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPref
.setPluginKey(getPluginKey())
.create();
}
}
@Override
public void onDestroy() {
@ -92,9 +120,15 @@ public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPref
ArrayList<String> paths = new ArrayList<>();
ArrayList<String> pathNames = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
List<StorageVolume> volumes = context.getSystemService(StorageManager.class).getStorageVolumes();
for (StorageVolume sv : volumes) {
pathNames.add(sv.getDescription(context));
paths.add(sv.getDirectory().getPath());
}
} else {
List<StorageInfo> storageInfoList = SftpSettingsFragment.getStorageInfoList(context, this);
Collections.sort(storageInfoList, Comparator.comparing(StorageInfo::getUri));
if (storageInfoList.size() > 0) {
getPathsAndNamesForStorageInfoList(paths, pathNames, storageInfoList);
} else {
@ -103,24 +137,28 @@ public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPref
device.sendPacket(np2);
return true;
}
removeChildren(storageInfoList);
server.setSafRoots(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
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 class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPref
@Override
public boolean hasSettings() {
return true;
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R;
}
@Override
@ -227,7 +265,6 @@ public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPref
@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();

View File

@ -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<SftpPlugin.StorageInfo> storageInfoList) {
safFileSystemFactory.initRoots(storageInfoList);
}
void init(Context context, Device device) throws GeneralSecurityException {
@ -78,8 +84,12 @@ class SimpleSftpServer {
}
});
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 @@ class SimpleSftpServer {
sshd.setPasswordAuthenticator(passwordAuth);
}
public boolean start(List<SftpPlugin.StorageInfo> storageInfoList) {
public boolean start() {
if (!started) {
fileSystemFactory.initRoots(storageInfoList);
passwordAuth.password = RandomHelper.randomString(28);
port = STARTPORT;

View File

@ -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 class StartActivityAlertDialogFragment extends AlertDialogFragment {
}
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 class StartActivityAlertDialogFragment extends AlertDialogFragment {
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 class StartActivityAlertDialogFragment extends AlertDialogFragment {
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);