Commit a6fdddf8 authored by Erik Duisters's avatar Erik Duisters

Use Storage Access Framework on SDK >= 21 (Lollipop and above)

Summary:
Use Storage Access Framework on Android running SDK >= 21 so writing to
sdcard will work again

|{F6546802}|{F6546803}|{F6546804}|
|API 21+|API 19-|Edit|

Test Plan:
Install patch on Android phone with Build.Version < 19 (Kitkat)

- Without a sdcard: Verify that dolphin displays an "All Files" entry that is empty
- With a sdcard and with "Add camera folder shortcut" turned off: Verify that dolphin displays the configured display name of the sdcard
- With a sdcard and with "Add camera folder shortcut" turned on: Verify that dolphin displays the configured display name of the sdcard and also lists a "Camera pictures" shortcut
- With a sdcard: Verify that when changing the display name or the "Add camera folder shortcut" preference dolphin displays the updated items (after pressing F5)
- With a sdcard: Verify that files can be read and written to/from the sdcard

Install patch on Android phone with Build.Version < 19 (Kitkat)
- Repeat the above tests except for the read/write test: Verify that files can be read from the sdcard

Install patch on Android phone with Build.Version > 21 (Lollipop)

- Without any configured storage locations: Verify dolphin displays an "All Files" entry that is empty
- With configured storage locations: Verify dolphin displays the display names of the configured storage locations and that entering a location displays the correct directory entries
- Make one or several changes to the configured storage locations: Verify dolphin displays the display names of the configured storage locations (after pressing F5) and that entering a location displays the correct directory entries

Reviewers: #kde_connect, albertvaka, sredman

Reviewed By: #kde_connect, albertvaka, sredman

Subscribers: albertvaka, sredman, kdeconnect

Tags: #kde_connect

Differential Revision: https://phabricator.kde.org/D18212
parent f48b5612
......@@ -65,6 +65,11 @@ dependencies {
repositories {
jcenter()
google()
/* Needed for org.apache.sshd debugging
maven {
url "https://jitpack.io"
}
*/
}
implementation 'androidx.media:media:1.0.1'
......@@ -77,6 +82,7 @@ dependencies {
implementation 'org.apache.sshd:sshd-core:0.14.0'
implementation 'org.apache.mina:mina-core:2.0.19' //For some reason, makes sshd-core:0.14.0 work without NIO, which isn't available until Android 8+
//implementation('com.github.bright:slf4android:0.1.6') { transitive = true } // For org.apache.sshd debugging
implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' //For SSL certificate generation
implementation 'com.jakewharton:butterknife:10.0.0'
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7,10l5,5 5,-5z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
<path android:fillColor="#FFF" android:pathData="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="?attr/dialogPreferredPadding"
android:paddingRight="?attr/dialogPreferredPadding"
android:paddingTop="10dp">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/storageLocation"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:cursorVisible="false"
android:hint="@string/sftp_storage_preference_storage_location"
android:lines="1"
android:longClickable="false"
android:maxLines="1"
android:scrollHorizontally="true"
android:ellipsize="end"
android:inputType="text"
android:text="@string/sftp_storage_preference_click_to_select"
android:textColor="@android:color/darker_gray"
android:editable="false"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/storageDisplayNameInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/storageDisplayName"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/sftp_storage_preference_display_name"
android:lines="1"
android:maxLines="1"
android:scrollHorizontally="true"
android:ellipsize="end"
android:inputType="text"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<CheckBox
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:background="@null"/>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/delete"
android:title="@string/sftp_action_mode_menu_delete"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_delete"/>
</menu>
\ No newline at end of file
......@@ -225,13 +225,37 @@
<string name="title_activity_notification_filter">Notification filter</string>
<string name="filter_apps_info">Notifications will be synchronized for the selected apps.</string>
<string name="sftp_internal_storage">Internal storage</string>
<string name="sftp_all_files">All files</string>
<string name="sftp_sdcard_num">SD card %d</string>
<string name="sftp_sdcard">SD card</string>
<string name="sftp_readonly">(read only)</string>
<string name="sftp_camera">Camera pictures</string>
<string name="add_device_dialog_title">Add device</string>
<string name="add_device_hint">Hostname or IP address</string>
<string name="sftp_preference_detected_sdcards">Detected SD cards</string>
<string name="sftp_preference_edit_sdcard_title">Edit SD card</string>
<string name="sftp_preference_configured_storage_locations">Configured storage locations</string>
<string name="sftp_preference_add_storage_location_title">Add storage location</string>
<string name="sftp_preference_edit_storage_location">Edit storage location</string>
<string name="sftp_preference_add_camera_shortcut">Add camera folder shortcut</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">Add a shortcut to the camera folder</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">Do not add a shortcut to the camera folder</string>
<string name="sftp_preference_key_preference_category" translatable="false">key_sftp_preference_category</string>
<string name="sftp_preference_key_add_storage" translatable="false">key_sftp_add_storage</string>
<string name="sftp_preference_key_add_camera_shortcut" translatable="false">key_sftp_add_camera_shotcut</string>
<string name="sftp_preference_key_storage_info" translatable="false">key_sftp_storage_info%d"</string>
<string name="sftp_preference_key_storage_info_list" translatable="false">key_sftp_storage_info_list</string>
<string name="sftp_storage_preference_storage_location">Storage location</string>
<string name="sftp_storage_preference_storage_location_already_configured">This location has already been configured</string>
<string name="sftp_storage_preference_click_to_select">click to select</string>
<string name="sftp_storage_preference_display_name">Display name</string>
<string name="sftp_storage_preference_display_name_already_used">This display name is already used</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">Display name cannot be empty</string>
<string name="sftp_action_mode_menu_delete">Delete</string>
<string name="sftp_no_sdcard_detected">No SD card detected</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="add_host">Add host/IP</string>
<string name="add_host_hint">Hostname or IP</string>
<string name="no_players_connected">No players found</string>
<string name="mpris_player_on_device">%1$s on %2$s</string>
<string name="send_files">Send files</string>
......@@ -262,7 +286,6 @@
<string name="permission_explanation">This plugin needs permissions to work</string>
<string name="optional_permission_explanation">You need to grant extra permissions to enable all functions</string>
<string name="plugins_need_optional_permission">Some plugins have features disabled because of lack of permission (tap for more info):</string>
<string name="sftp_permission_explanation">To access your files from your PC the app needs permission to access your phone\'s storage</string>
<string name="share_optional_permission_explanation">To share files between your phone and your desktop you need to give access to the phone\'s storage</string>
<string name="telepathy_permission_explanation">To read and write SMS from your desktop you need to give permission to SMS</string>
<string name="telephony_permission_explanation">To see phone calls and SMS from the desktop you need to give permission to phone calls and SMS</string>
......
......@@ -18,6 +18,7 @@
<item name="android:textColorPrimary">@android:color/black</item>
<item name="android:textColor">@android:color/black</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="actionModeStyle">@style/ActionModeStyle</item>
</style>
<style name="KdeConnectThemeBase.NoActionBar" parent="KdeConnectThemeBase">
......@@ -42,4 +43,8 @@
<style name="DisableableButton" parent="ThemeOverlay.AppCompat">
<item name="colorButtonNormal">@drawable/disableable_button</item>
</style>
<style name="ActionModeStyle" parent="Widget.AppCompat.ActionMode">
<item name="background">@color/primaryDark</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="@string/sftp_preference_key_preference_category"
android:title="@string/sftp_preference_detected_sdcards"
android:persistent="false">
</PreferenceCategory>
<org.kde.kdeconnect.Plugins.SftpPlugin.StoragePreference
android:key="key_sftp_add_storage"
android:icon="@drawable/ic_add"
android:title="@string/sftp_preference_add_storage_location_title"
android:persistent="false"/>
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="true"
android:key="@string/sftp_preference_key_add_camera_shortcut"
android:summaryOff="@string/sftp_preference_add_camera_shortcut_summary_off"
android:summaryOn="@string/sftp_preference_add_camera_shortcut_summary_on"
android:title="@string/sftp_preference_add_camera_shortcut"/>
</PreferenceScreen>
\ No newline at end of file
......@@ -20,7 +20,12 @@
package org.kde.kdeconnect.Helpers;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import java.io.BufferedReader;
import java.io.File;
......@@ -32,6 +37,8 @@ import java.util.List;
import java.util.Scanner;
import java.util.StringTokenizer;
import androidx.annotation.NonNull;
//Code from http://stackoverflow.com/questions/9340332/how-can-i-get-the-list-of-mounted-external-storage-of-android-device/19982338#19982338
//modified to work on Lollipop and other devices
public class StorageHelper {
......@@ -43,7 +50,7 @@ public class StorageHelper {
public final boolean removable;
public final int number;
StorageInfo(String path, boolean readonly, boolean removable, int number) {
public StorageInfo(String path, boolean readonly, boolean removable, int number) {
this.path = path;
this.readonly = readonly;
this.removable = removable;
......@@ -77,7 +84,7 @@ public class StorageHelper {
}
File storage = new File("/storage/");
if (storage.exists() && storage.isDirectory()) {
if (storage.exists() && storage.isDirectory() && storage.canRead()) {
String mounts = null;
try (Scanner scanner = new Scanner(new File("/proc/mounts"))) {
mounts = scanner.useDelimiter("\\A").next();
......@@ -100,7 +107,7 @@ public class StorageHelper {
if (!path.startsWith("/storage/emulated") || dirs.length == 1) {
if (!paths.contains(path) && !paths.contains(path2)) {
if (mounts == null || mounts.contains(path) || mounts.contains(path2)) {
list.add(0, new StorageInfo(path, false, true, cur_removable_number++));
list.add(0, new StorageInfo(path, dir.canWrite(), true, cur_removable_number++));
paths.add(path);
}
}
......@@ -153,4 +160,37 @@ public class StorageHelper {
return list;
}
/* treeUri documentId
* ==================================================================================================
* content://com.android.providers.downloads.documents/tree/downloads => downloads
* content://com.android.externalstorage.documents/tree/1715-1D1F: => 1715-1D1F:
* content://com.android.externalstorage.documents/tree/1715-1D1F:My%20Photos => 1715-1D1F:My Photos
* content://com.android.externalstorage.documents/tree/primary: => primary:
* content://com.android.externalstorage.documents/tree/primary:DCIM => primary:DCIM
* content://com.android.externalstorage.documents/tree/primary:Download/bla => primary:Download/bla
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static String getDisplayName(@NonNull Context context, @NonNull Uri treeUri) {
List<String> pathSegments = treeUri.getPathSegments();
if (!pathSegments.get(0).equals("tree")) {
throw new IllegalArgumentException("treeUri is not valid");
}
String documentId = DocumentsContract.getTreeDocumentId(treeUri);
int colonIdx = pathSegments.get(1).indexOf(':');
if (colonIdx >= 0) {
String tree = pathSegments.get(1).substring(0, colonIdx + 1);
if (!documentId.equals(tree)) {
return documentId.substring(tree.length());
} else {
return documentId.substring(0, colonIdx);
}
}
return documentId;
}
}
/*
* Copyright 2018 Erik Duisters <e.duisters1@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Plugins.SftpPlugin;
import android.content.Context;
import android.os.Build;
import org.apache.sshd.common.Session;
import org.apache.sshd.common.file.FileSystemFactory;
import org.apache.sshd.common.file.FileSystemView;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class AndroidFileSystemFactory implements FileSystemFactory {
final private Context context;
final Map<String, String> roots;
AndroidFileSystemFactory(Context context) {
this.context = context;
this.roots = new HashMap<>();
}
void initRoots(List<SftpPlugin.StorageInfo> storageInfoList) {
for (SftpPlugin.StorageInfo curStorageInfo : storageInfoList) {
if (curStorageInfo.isFileUri()) {
if (curStorageInfo.uri.getPath() != null){
roots.put(curStorageInfo.displayName, curStorageInfo.uri.getPath());
}
} else if (curStorageInfo.isContentUri()){
roots.put(curStorageInfo.displayName, curStorageInfo.uri.toString());
}
}
}
@Override
public FileSystemView createFileSystemView(final Session username) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
if (roots.size() == 0) {
throw new RuntimeException("roots cannot be empty");
}
String[] rootsAsString = new String[roots.size()];
roots.keySet().toArray(rootsAsString);
return new AndroidFileSystemView(roots, rootsAsString[0], username.getUsername(), context);
} else {
return new AndroidSafFileSystemView(roots, username.getUsername(), context);
}
}
}
/*
* Copyright 2018 Erik Duisters <e.duisters1@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Plugins.SftpPlugin;
import android.content.Context;
import org.apache.sshd.common.file.FileSystemView;
import org.apache.sshd.common.file.SshFile;
import org.apache.sshd.common.file.nativefs.NativeFileSystemView;
import org.apache.sshd.common.file.nativefs.NativeSshFile;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class AndroidFileSystemView extends NativeFileSystemView {
final private String userName;
final private Context context;
private final Map<String, String> roots;
private final RootFile rootFile;
AndroidFileSystemView(Map<String, String> roots, String currentRoot, final String userName, Context context) {
super(userName, roots, currentRoot, File.separatorChar, true);
this.roots = roots;
this.userName = userName;
this.context = context;
this.rootFile = new RootFile( createFileList(), userName, true);
}
private List<SshFile> createFileList() {
List<SshFile> list = new ArrayList<>();
for (Map.Entry<String, String> entry : roots.entrySet()) {
String displayName = entry.getKey();
String path = entry.getValue();
list.add(createNativeSshFile(displayName, new File(path), userName));
}
return list;
}
@Override
public SshFile getFile(String file) {
return getFile("/", file);
}
@Override
public SshFile getFile(SshFile baseDir, String file) {
return getFile(baseDir.getAbsolutePath(), file);
}
@Override
protected SshFile getFile(String dir, String file) {
if (!dir.endsWith("/")) {
dir = dir + "/";
}
if (!file.startsWith("/")) {
file = dir + file;
}
String filename = NativeSshFile.getPhysicalName("/", "/", file, false);
if (filename.equals("/")) {
return rootFile;
}
for (String root : roots.keySet()) {
if (filename.indexOf(root) == 1) {
String nameWithoutRoot = filename.substring(root.length() + 1);
String path = roots.get(root);
if (nameWithoutRoot.isEmpty()) {
return createNativeSshFile(filename, new File(path), userName);
} else {
return createNativeSshFile(filename, new File(path, nameWithoutRoot), userName);
}
}
}
//It's a file under / but not one covered by any Tree
return new RootFile(new ArrayList<>(0), userName, false);
}
// NativeFileSystemView.getFile(), NativeSshFile.getParentFile() and NativeSshFile.listSshFiles() call
// createNativeSshFile to create new NativeSshFiles so override that instead of getFile() to always create an AndroidSshFile
@Override
public AndroidSshFile createNativeSshFile(String name, File file, String username) {
return new AndroidSshFile(this, name, file, username, context);
}
@Override
public FileSystemView getNormalizedView() {
return this;
}
}
/*
* Copyright 2018 Erik Duisters <e.duisters1@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Plugins.SftpPlugin;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.provider.DocumentsContract;
import org.apache.sshd.common.file.FileSystemView;
import org.apache.sshd.common.file.SshFile;
import org.apache.sshd.common.file.nativefs.NativeSshFile;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@TargetApi(21)
public class AndroidSafFileSystemView implements FileSystemView {
final String userName;
final Context context;
private final Map<String, String> roots;
private final RootFile rootFile;
AndroidSafFileSystemView(Map<String, String> roots, String userName, Context context) {
this.roots = roots;
this.userName = userName;
this.context = context;
this.rootFile = new RootFile( createFileList(), userName, true);
}
private List<SshFile> createFileList() {
List<SshFile> list = new ArrayList<>();
for (Map.Entry<String, String> entry : roots.entrySet()) {
String displayName = entry.getKey();
String uri = entry.getValue();
Uri treeUri = Uri.parse(uri);
Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri));
list.add(createAndroidSafSshFile(null, documentUri, File.separatorChar + displayName));
}
return list;
}
@Override
public SshFile getFile(String file) {
return getFile("/", file);
}
@Override
public SshFile getFile(SshFile baseDir, String file) {
return getFile(baseDir.getAbsolutePath(), file);
}
protected SshFile getFile(String dir, String file) {
if (!dir.endsWith("/")) {
dir = dir + "/";
}
if (!file.startsWith("/")) {
file = dir + file;
}
String filename = NativeSshFile.getPhysicalName("/", "/", file, false);
if (filename.equals("/")) {
return rootFile;
}
for (String root : roots.keySet()) {
if (filename.indexOf(root) == 1) {
String nameWithoutRoot = filename.substring(root.length() + 1);
String pathOrUri = roots.get(root);
Uri treeUri = Uri.parse(pathOrUri);
if (nameWithoutRoot.isEmpty()) {
//TreeDocument
Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri));
return createAndroidSafSshFile(documentUri, documentUri, filename);
} else {
//ChildDocument, strip the leading / from nameWithoutRoot and append that to the treeDocumentId
String treeDocumentId = DocumentsContract.getTreeDocumentId(treeUri);
File nameWithoutRootFile = new File(nameWithoutRoot);
String parentSuffix = nameWithoutRootFile.getParent();
String parentDocumentId = treeDocumentId + (parentSuffix.equals("/") ? "" : parentSuffix.substring(1));
Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, parentDocumentId);
Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, treeDocumentId + nameWithoutRoot.substring(1));
return createAndroidSafSshFile(parentUri, documentUri, filename);
}
}
}
//It's a file under / but not one covered by any Tree
return new RootFile(new ArrayList<>(0), userName, false);
}
public AndroidSafSshFile createAndroidSafSshFile(Uri parentUri, Uri documentUri, String virtualFilename) {
return new AndroidSafSshFile(this, parentUri, documentUri, virtualFilename);
}
@Override
public FileSystemView getNormalizedView() {
return this;
}
}
/*
* Copyright 2018 Erik Duisters <e.duisters1@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Plugins.SftpPlugin;
import android.content.Context;
import android.net.Uri;
import org.apache.sshd.common.file.nativefs.NativeSshFile;
import org.kde.kdeconnect.Helpers.MediaStoreHelper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;