2014-11-16 23:14:06 -08:00
/ *
2020-08-17 16:17:20 +02:00
* SPDX - FileCopyrightText : 2014 Samoilenko Yuri < kinnalru @gmail.com >
2014-11-16 23:14:06 -08:00
*
2020-08-17 16:17:20 +02:00
* SPDX - License - Identifier : GPL - 2 . 0 - only OR GPL - 3 . 0 - only OR LicenseRef - KDE - Accepted - GPL
2014-11-16 23:14:06 -08:00
* /
2014-01-06 19:30:55 +04:00
package org.kde.kdeconnect.Plugins.SftpPlugin ;
2019-03-08 13:44:54 +01:00
import android.app.Activity ;
import android.content.ContentResolver ;
import android.content.SharedPreferences ;
import android.net.Uri ;
2019-03-31 20:09:44 +02:00
import android.util.Log ;
2014-01-06 19:30:55 +04:00
2020-03-20 20:30:58 +00:00
import androidx.annotation.NonNull ;
2019-03-08 13:44:54 +01:00
import org.json.JSONException ;
import org.json.JSONObject ;
2018-03-04 11:31:37 +01:00
import org.kde.kdeconnect.NetworkPacket ;
2014-01-06 19:30:55 +04:00
import org.kde.kdeconnect.Plugins.Plugin ;
2019-02-11 20:04:40 +01:00
import org.kde.kdeconnect.Plugins.PluginFactory ;
2019-03-08 13:44:54 +01:00
import org.kde.kdeconnect.UserInterface.AlertDialogFragment ;
import org.kde.kdeconnect.UserInterface.DeviceSettingsAlertDialogFragment ;
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment ;
2014-01-06 19:30:55 +04:00
import org.kde.kdeconnect_tp.R ;
2014-10-10 15:49:27 -07:00
import java.util.ArrayList ;
2019-03-08 13:44:54 +01:00
import java.util.Collections ;
2020-07-01 08:01:02 +05:30
import java.util.Comparator ;
2019-03-08 13:44:54 +01:00
import java.util.Iterator ;
2014-10-10 15:49:27 -07:00
import java.util.List ;
2019-02-11 20:04:40 +01:00
@PluginFactory.LoadablePlugin
2019-03-08 13:44:54 +01:00
public class SftpPlugin extends Plugin implements SharedPreferences . OnSharedPreferenceChangeListener {
2014-01-06 19:30:55 +04:00
2018-10-26 23:53:58 +02:00
private final static String PACKET_TYPE_SFTP = " kdeconnect.sftp " ;
private final static String PACKET_TYPE_SFTP_REQUEST = " kdeconnect.sftp.request " ;
2016-05-31 17:19:39 +02:00
2020-03-20 20:30:58 +00:00
static int PREFERENCE_KEY_STORAGE_INFO_LIST = R . string . sftp_preference_key_storage_info_list ;
2014-01-06 19:30:55 +04:00
2020-03-20 20:30:58 +00:00
private static final SimpleSftpServer server = new SimpleSftpServer ( ) ;
2019-03-08 13:44:54 +01:00
2014-01-06 19:30:55 +04:00
@Override
public String getDisplayName ( ) {
return context . getResources ( ) . getString ( R . string . pref_plugin_sftp ) ;
}
@Override
public String getDescription ( ) {
return context . getResources ( ) . getString ( R . string . pref_plugin_sftp_desc ) ;
}
@Override
public boolean onCreate ( ) {
2018-08-31 21:12:04 +02:00
try {
server . init ( context , device ) ;
return true ;
} catch ( Exception e ) {
2019-03-31 20:09:44 +02:00
Log . e ( " SFTP " , " Exception in server.init() " , e ) ;
2018-08-31 21:12:04 +02:00
return false ;
}
2014-01-06 19:30:55 +04:00
}
2019-03-08 13:44:54 +01:00
@Override
2023-03-05 22:04:34 +01:00
public boolean checkRequiredPermissions ( ) {
2023-03-05 16:15:03 +00:00
return SftpSettingsFragment . getStorageInfoList ( context , this ) . size ( ) ! = 0 ;
2019-03-08 13:44:54 +01:00
}
@Override
2023-03-05 22:04:34 +01:00
public AlertDialogFragment getPermissionExplanationDialog ( ) {
2019-03-08 13:44:54 +01:00
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 ( ) ;
}
2014-01-06 19:30:55 +04:00
@Override
public void onDestroy ( ) {
2014-01-07 17:40:21 +04:00
server . stop ( ) ;
2020-03-20 20:30:58 +00:00
if ( preferences ! = null ) {
preferences . unregisterOnSharedPreferenceChangeListener ( this ) ;
}
2014-01-06 19:30:55 +04:00
}
@Override
2018-03-04 11:31:37 +01:00
public boolean onPacketReceived ( NetworkPacket np ) {
2014-01-06 19:30:55 +04:00
if ( np . getBoolean ( " startBrowsing " ) ) {
2019-03-08 13:44:54 +01:00
ArrayList < String > paths = new ArrayList < > ( ) ;
ArrayList < String > pathNames = new ArrayList < > ( ) ;
2020-03-20 20:30:58 +00:00
List < StorageInfo > storageInfoList = SftpSettingsFragment . getStorageInfoList ( context , this ) ;
2020-07-01 08:01:02 +05:30
Collections . sort ( storageInfoList , Comparator . comparing ( StorageInfo : : getUri ) ) ;
2019-03-08 13:44:54 +01:00
if ( storageInfoList . size ( ) > 0 ) {
getPathsAndNamesForStorageInfoList ( paths , pathNames , storageInfoList ) ;
} else {
NetworkPacket np2 = new NetworkPacket ( PACKET_TYPE_SFTP ) ;
2023-03-05 16:15:03 +00:00
np2 . set ( " errorMessage " , context . getString ( R . string . sftp_no_storage_locations_configured ) ) ;
2019-03-08 13:44:54 +01:00
device . sendPacket ( np2 ) ;
return true ;
}
removeChildren ( storageInfoList ) ;
if ( server . start ( storageInfoList ) ) {
2020-03-20 20:30:58 +00:00
if ( preferences ! = null ) {
preferences . registerOnSharedPreferenceChangeListener ( this ) ;
}
2014-10-10 15:49:27 -07:00
2018-03-04 11:31:37 +01:00
NetworkPacket np2 = new NetworkPacket ( PACKET_TYPE_SFTP ) ;
2014-10-10 15:49:27 -07:00
2019-03-08 13:44:54 +01:00
//TODO: ip is not used on desktop any more remove both here and from desktop code when nobody ships 1.2.0
2014-01-07 17:40:21 +04:00
np2 . set ( " ip " , server . getLocalIpAddress ( ) ) ;
2016-12-11 13:31:17 +01:00
np2 . set ( " port " , server . getPort ( ) ) ;
np2 . set ( " user " , SimpleSftpServer . USER ) ;
np2 . set ( " password " , server . getPassword ( ) ) ;
2014-10-10 15:49:27 -07:00
2015-03-15 17:52:14 -07:00
//Kept for compatibility, in case "multiPaths" is not possible or the other end does not support it
2019-03-08 13:44:54 +01:00
np2 . set ( " path " , " / " ) ;
2014-10-10 15:49:27 -07:00
2017-05-31 15:51:07 +02:00
if ( paths . size ( ) > 0 ) {
np2 . set ( " multiPaths " , paths ) ;
np2 . set ( " pathNames " , pathNames ) ;
}
2018-03-04 11:31:37 +01:00
device . sendPacket ( np2 ) ;
2014-10-10 15:49:27 -07:00
2014-01-06 19:30:55 +04:00
return true ;
}
}
return false ;
}
2019-03-08 13:44:54 +01:00
private void getPathsAndNamesForStorageInfoList ( List < String > paths , List < String > pathNames , List < StorageInfo > storageInfoList ) {
StorageInfo prevInfo = null ;
StringBuilder pathBuilder = new StringBuilder ( ) ;
2020-03-20 20:30:58 +00:00
2019-03-08 13:44:54 +01:00
for ( StorageInfo curInfo : storageInfoList ) {
pathBuilder . setLength ( 0 ) ;
pathBuilder . append ( " / " ) ;
if ( prevInfo ! = null & & curInfo . uri . toString ( ) . startsWith ( prevInfo . uri . toString ( ) ) ) {
pathBuilder . append ( prevInfo . displayName ) ;
pathBuilder . append ( " / " ) ;
if ( curInfo . uri . getPath ( ) ! = null & & prevInfo . uri . getPath ( ) ! = null ) {
pathBuilder . append ( curInfo . uri . getPath ( ) . substring ( prevInfo . uri . getPath ( ) . length ( ) ) ) ;
} else {
throw new RuntimeException ( " curInfo.uri.getPath() or parentInfo.uri.getPath() returned null " ) ;
}
} else {
pathBuilder . append ( curInfo . displayName ) ;
if ( prevInfo = = null | | ! curInfo . uri . toString ( ) . startsWith ( prevInfo . uri . toString ( ) ) ) {
prevInfo = curInfo ;
}
}
paths . add ( pathBuilder . toString ( ) ) ;
pathNames . add ( curInfo . displayName ) ;
}
}
private void removeChildren ( List < StorageInfo > storageInfoList ) {
StorageInfo prevInfo = null ;
Iterator < StorageInfo > it = storageInfoList . iterator ( ) ;
while ( it . hasNext ( ) ) {
StorageInfo curInfo = it . next ( ) ;
if ( prevInfo ! = null & & curInfo . uri . toString ( ) . startsWith ( prevInfo . uri . toString ( ) ) ) {
it . remove ( ) ;
} else {
if ( prevInfo = = null | | ! curInfo . uri . toString ( ) . startsWith ( prevInfo . uri . toString ( ) ) ) {
prevInfo = curInfo ;
}
}
2018-11-07 16:05:53 +01:00
}
2017-05-31 15:51:07 +02:00
}
2015-09-08 14:54:04 -07:00
@Override
2018-03-04 11:31:37 +01:00
public String [ ] getSupportedPacketTypes ( ) {
return new String [ ] { PACKET_TYPE_SFTP_REQUEST } ;
2015-09-08 14:54:04 -07:00
}
@Override
2018-03-04 11:31:37 +01:00
public String [ ] getOutgoingPacketTypes ( ) {
return new String [ ] { PACKET_TYPE_SFTP } ;
2015-09-08 14:54:04 -07:00
}
2019-03-08 13:44:54 +01:00
@Override
public boolean hasSettings ( ) {
return true ;
}
2020-03-20 20:30:58 +00:00
@Override
public boolean supportsDeviceSpecificSettings ( ) { return true ; }
@Override
public void copyGlobalToDeviceSpecificSettings ( SharedPreferences globalSharedPreferences ) {
String KeyStorageInfoList = context . getString ( PREFERENCE_KEY_STORAGE_INFO_LIST ) ;
2023-03-05 16:15:03 +00:00
if ( this . preferences ! = null & & ! this . preferences . contains ( KeyStorageInfoList ) ) {
2020-03-20 20:30:58 +00:00
this . preferences
. edit ( )
. putString ( KeyStorageInfoList , globalSharedPreferences . getString ( KeyStorageInfoList , " [] " ) )
. apply ( ) ;
}
}
@Override
public void removeSettings ( SharedPreferences sharedPreferences ) {
sharedPreferences
. edit ( )
. remove ( context . getString ( PREFERENCE_KEY_STORAGE_INFO_LIST ) )
. apply ( ) ;
}
2019-03-08 13:44:54 +01:00
@Override
public PluginSettingsFragment getSettingsFragment ( Activity activity ) {
2022-12-28 19:24:07 +01:00
return SftpSettingsFragment . newInstance ( getPluginKey ( ) , R . xml . sftpplugin_preferences ) ;
2019-03-08 13:44:54 +01:00
}
@Override
public void onSharedPreferenceChanged ( SharedPreferences sharedPreferences , String key ) {
2023-03-05 16:15:03 +00:00
if ( key . equals ( context . getString ( PREFERENCE_KEY_STORAGE_INFO_LIST ) ) ) {
2019-03-08 13:44:54 +01:00
//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 ( ) ;
NetworkPacket np = new NetworkPacket ( PACKET_TYPE_SFTP_REQUEST ) ;
np . set ( " startBrowsing " , true ) ;
onPacketReceived ( np ) ;
}
}
}
static class StorageInfo {
private static final String KEY_DISPLAY_NAME = " DisplayName " ;
private static final String KEY_URI = " Uri " ;
@NonNull String displayName ;
@NonNull Uri uri ;
StorageInfo ( @NonNull String displayName , @NonNull Uri uri ) {
this . displayName = displayName ;
this . uri = uri ;
}
2020-07-01 08:01:02 +05:30
@NonNull
Uri getUri ( ) {
return uri ;
}
2019-03-08 13:44:54 +01:00
static StorageInfo copy ( StorageInfo from ) {
//Both String and Uri are immutable
return new StorageInfo ( from . displayName , from . uri ) ;
}
boolean isFileUri ( ) {
return uri . getScheme ( ) . equals ( ContentResolver . SCHEME_FILE ) ;
}
boolean isContentUri ( ) {
return uri . getScheme ( ) . equals ( ContentResolver . SCHEME_CONTENT ) ;
}
public JSONObject toJSON ( ) throws JSONException {
JSONObject jsonObject = new JSONObject ( ) ;
jsonObject . put ( KEY_DISPLAY_NAME , displayName ) ;
jsonObject . put ( KEY_URI , uri . toString ( ) ) ;
return jsonObject ;
}
@NonNull
static StorageInfo fromJSON ( @NonNull JSONObject jsonObject ) throws JSONException {
String displayName = jsonObject . getString ( KEY_DISPLAY_NAME ) ;
Uri uri = Uri . parse ( jsonObject . getString ( KEY_URI ) ) ;
return new StorageInfo ( displayName , uri ) ;
}
@Override
public boolean equals ( Object o ) {
if ( this = = o ) return true ;
if ( o = = null | | getClass ( ) ! = o . getClass ( ) ) return false ;
StorageInfo that = ( StorageInfo ) o ;
if ( ! displayName . equals ( that . displayName ) ) return false ;
return uri . equals ( that . uri ) ;
}
@Override
public int hashCode ( ) {
int result = displayName . hashCode ( ) ;
result = 31 * result + uri . hashCode ( ) ;
return result ;
}
}
2014-01-06 19:30:55 +04:00
}