diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a480d7bf..5c9f16a8 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 3a86100a..878a4ab1 100644
--- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java
@@ -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,19 +74,36 @@ public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPref
@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 class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPref
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 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();
diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java
index d7ba011a..de60bf78 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 @@ 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 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 62a642a7..1a481a4a 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 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);