2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-09-01 06:35:09 +00:00

Compare commits

...

32 Commits

Author SHA1 Message Date
Albert Vaca
a918f0cfe6 Bump version to release 2019-03-20 23:12:11 +01:00
Albert Vaca
aada931d6a Re-enable SMS plugin now that we know the secrets to approval 2019-03-20 23:11:58 +01:00
Albert Vaca
eed77c530b Revert "Stop using ClassIndex, it seems to be causing problems"
This reverts commit 7799f7e817.
2019-03-20 22:35:27 +01:00
Albert Vaca
3b7edf2d2f Merge branch 'master' into play-store 2019-03-20 22:28:53 +01:00
Albert Vaca
37657388c6 Uppercasing some stuff 2019-03-20 22:14:27 +01:00
Albert Vaca
360e4bc1cb Fix ConcurrentModificationException
When onPacketReceived was called at the same time as getSinks, because
getSinks uses .values() and onPacketReceived does .put()
2019-03-20 22:12:53 +01:00
Albert Vaca
b689548aa9 Remove private API usage 2019-03-20 22:07:02 +01:00
Albert Vaca
adac026dfa Change RunCommand to runWithPlugin 2019-03-20 22:06:45 +01:00
Nicolas Fella
d513a5305a Show device name in MPRIS notification 2019-03-20 20:50:06 +00:00
Albert Vaca
4d38f9753c Disable plugin that breaks on Android 4 2019-03-20 09:42:32 +01:00
Albert Vaca
18548fb6df Added missing "unregister" 2019-03-20 09:36:36 +01:00
l10n daemon script
fd5738aa99 GIT_SILENT made messages (after extraction) 2019-03-20 03:06:17 +01:00
Albert Vaca Cintora
7799f7e817 Stop using ClassIndex, it seems to be causing problems 2019-03-19 22:50:22 +01:00
Albert Vaca
8db8937927 Update version to release (to Play Store only) 2019-03-19 09:03:30 +01:00
Albert Vaca
13e6f2d250 Emergency update to fix Play Store's policy violation
- Make TelephonyPlugin only handle calls and not texts
- Disable SMSPlugin.
- Stop using READ_CALL_LOG permission (for contact names)
2019-03-19 09:00:45 +01:00
l10n daemon script
34db7f682f GIT_SILENT made messages (after extraction) 2019-03-19 03:07:26 +01:00
Nicolas Fella
f7763bf5a9 Request READ_CALL_LOG permission 2019-03-18 01:34:51 +00:00
Nicolas Fella
fb2859c24d Prompt user to enable remote keyboard 2019-03-17 14:42:16 +00:00
Nicolas Fella
145fa4f009 Remove dead code 2019-03-17 14:41:49 +00:00
l10n daemon script
e7f203ee3a GIT_SILENT made messages (after extraction) 2019-03-17 03:05:08 +01:00
l10n daemon script
c9feafb982 GIT_SILENT made messages (after extraction) 2019-03-14 03:01:50 +01:00
l10n daemon script
18c344bbc2 GIT_SILENT made messages (after extraction) 2019-03-13 03:10:45 +01:00
l10n daemon script
d73c8e914f GIT_SILENT made messages (after extraction) 2019-03-12 03:04:33 +01:00
l10n daemon script
86f4803083 GIT_SILENT made messages (after extraction) 2019-03-11 03:06:09 +01:00
l10n daemon script
eb875dceb0 GIT_SILENT made messages (after extraction) 2019-03-10 03:07:44 +01:00
Matthijs Tijink
3e85dd6160 Add all missing MPRIS properties in the media session control
Also simplified some code.
2019-03-09 20:06:32 +00:00
Matthijs Tijink
f9bc3f8e0b Block KDE Connect as media session
Also re-enable the media session mpris remote again
2019-03-09 13:57:33 +00:00
l10n daemon script
35ac407aee GIT_SILENT made messages (after extraction) 2019-03-09 03:06:44 +01:00
Albert Vaca Cintora
e033aad425 Bump version to release 2019-03-08 19:31:27 +01:00
Erik Duisters
f2e505b8af Allow shares from desktop to be canceled
Summary:
Allow in progress file transfers to be canceled

BUG: 349956

{F6373048}

{F6373050}

{F6373051}

Test Plan:
Send a large file from desktop to android
Press cancel in the progress notification

Result: the file transfer is cancelled and the cancelled file is deleted from storage

Reviewers: #kde_connect, nicolasfella, albertvaka

Reviewed By: #kde_connect, albertvaka

Subscribers: albertvaka, nicolasfella, kdeconnect

Tags: #kde_connect

Differential Revision: https://phabricator.kde.org/D16491
2019-03-08 19:08:29 +01:00
Erik Duisters
a6fdddf843 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
2019-03-08 15:02:10 +01:00
Albert Vaca Cintora
f48b5612c7 Fragments must be public classes
Otherwise they crash at runtime when launched
2019-03-08 14:50:35 +01:00
81 changed files with 3430 additions and 519 deletions

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.kde.kdeconnect_tp"
android:versionCode="11100"
android:versionName="1.11">
android:versionCode="11240"
android:versionName="1.12.4">
<supports-screens
android:anyDensity="true"
@@ -22,15 +22,16 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<application
android:icon="@drawable/icon"
@@ -224,6 +225,12 @@
android:value="org.kde.kdeconnect.Plugins.SharePlugin.ShareChooserTargetService" />
</activity>
<receiver android:name="org.kde.kdeconnect.Plugins.SharePlugin.ShareBroadcastReceiver">
<intent-filter>
<action android:name="org.kde.kdeconnect.Plugins.SharePlugin.CancelShare" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="org.kde.kdeconnect_tp.fileprovider"

View File

@@ -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'

View File

@@ -0,0 +1,9 @@
<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>

View File

@@ -1,9 +1,7 @@
<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>

View File

@@ -0,0 +1,58 @@
<?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>

View File

@@ -0,0 +1,9 @@
<?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"/>

View File

@@ -0,0 +1,10 @@
<?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>

View File

@@ -127,11 +127,11 @@
<string name="title_activity_notification_filter">مرشّح الإخطارات</string>
<string name="filter_apps_info">ستُزامن الإخطارات من التّطبيقات المحدّدة.</string>
<string name="sftp_internal_storage">التّخزين الدّاخليّ</string>
<string name="sftp_all_files">كلّ الملفّات</string>
<string name="sftp_sdcard_num">بطاقة SD %d</string>
<string name="sftp_sdcard">بطاقة SD</string>
<string name="sftp_readonly">(للقراءة فقط)</string>
<string name="sftp_camera">صور الكاميرا</string>
<string name="add_host">أضف مضيفًا/م​إ</string>
<string name="no_players_connected">لم يُعثر على مشغّلات</string>
<string name="mpris_player_on_device">%1$s على %2$s</string>
<string name="send_files">أرسل ملفّات</string>

View File

@@ -106,11 +106,11 @@
<string name="title_activity_notification_filter">Filter napomena</string>
<string name="filter_apps_info">Notifikacije će biti sinhronizovane s izabranim aplikacijama.</string>
<string name="sftp_internal_storage">Unutrašnji smještaj</string>
<string name="sftp_all_files">Sve fajlove</string>
<string name="sftp_sdcard_num">SD kartica %d</string>
<string name="sftp_sdcard">SD kartica</string>
<string name="sftp_readonly">(samo za čitanje)</string>
<string name="sftp_camera">Slike sa kamere</string>
<string name="add_host">Dodaj host/IP</string>
<string name="no_players_connected">Nema nađenih igrača</string>
<string name="mpris_player_on_device">%1$s na %2$s</string>
</resources>

View File

@@ -5,7 +5,7 @@
<string name="foreground_notification_devices">Connectat a: %s</string>
<string name="pref_plugin_telephony">Notificador de la telefonia</string>
<string name="pref_plugin_telephony_desc">Envia notificacions per a les trucades entrants</string>
<string name="pref_plugin_battery">Informe de la bateria</string>
<string name="pref_plugin_battery">Informa de la bateria</string>
<string name="pref_plugin_battery_desc">Informa periòdicament sobre l\'estat de la bateria</string>
<string name="pref_plugin_sftp">Exposa el sistema de fitxers</string>
<string name="pref_plugin_sftp_desc">Permet navegar de forma remota pel sistema de fitxers del dispositiu</string>
@@ -14,9 +14,9 @@
<string name="pref_plugin_mousepad">Entrada remota</string>
<string name="pref_plugin_mousepad_desc">Usa el vostre telèfon o tauleta com un ratolí i un teclat</string>
<string name="pref_plugin_presenter">Presentació de diapositives remota</string>
<string name="pref_plugin_presenter_desc">Useu el dispositiu per canviar les dispositives d\'una presentació</string>
<string name="pref_plugin_remotekeyboard">S\'estan rebent pulsacions de tecla remotes</string>
<string name="pref_plugin_remotekeyboard_desc">S\'estan rebent esdeveniments de pulsacions de tecla des de dispositius remots</string>
<string name="pref_plugin_presenter_desc">Usa el dispositiu per a canviar les dispositives d\'una presentació</string>
<string name="pref_plugin_remotekeyboard">Rep les pulsacions remotes de tecla</string>
<string name="pref_plugin_remotekeyboard_desc">Rep els esdeveniments de pulsacions de tecla des de dispositius remots</string>
<string name="pref_plugin_mpris">Controls multimèdia</string>
<string name="pref_plugin_mpris_desc">Proporciona un comandament a distància pel reproductor multimèdia</string>
<string name="pref_plugin_runcommand">Executa una ordre</string>
@@ -24,10 +24,10 @@
<string name="pref_plugin_contacts">Sincronitzador dels contactes</string>
<string name="pref_plugin_contacts_desc">Permet sincronitzar la llibreta de contactes del dispositiu</string>
<string name="pref_plugin_ping">Ping</string>
<string name="pref_plugin_ping_desc">Envia i rep els pings</string>
<string name="pref_plugin_ping_desc">Envia i rep els «ping»</string>
<string name="pref_plugin_notifications">Sincronitza les notificacions</string>
<string name="pref_plugin_notifications_desc">Accedeix a les vostres notificacions des d\'altres dispositius</string>
<string name="pref_plugin_receive_notifications">Recepció de les notificacions</string>
<string name="pref_plugin_receive_notifications">Rep les notificacions</string>
<string name="pref_plugin_receive_notifications_desc">Rep notificacions des d\'altres dispositius i mostrar-los a l\'Android</string>
<string name="pref_plugin_sharereceiver">Comparteix i rep</string>
<string name="pref_plugin_sharereceiver_desc">Comparteix els fitxers i els URL entre els dispositius</string>
@@ -38,6 +38,7 @@
<string name="open_settings">Obre l\'arranjament</string>
<string name="no_permissions">Us caldrà concedir permís per accedir a les notificacions</string>
<string name="no_permission_mprisreceiver">Per a poder controlar els reproductors multimèdia cal atorgar accés a les notificacions</string>
<string name="no_permissions_remotekeyboard">Per a rebre les pulsacions de tecles, haureu d\'activar el teclat remot del KDE Connect</string>
<string name="send_ping">Envia un ping</string>
<string name="open_mpris_controls">Control multimèdia</string>
<string name="remotekeyboard_editing_only_title">Fes servir les tecles remotes només en editar</string>
@@ -184,13 +185,32 @@
<string name="title_activity_notification_filter">Filtre per a les notificacions</string>
<string name="filter_apps_info">Les notificacions se sincronitzaran per a les aplicacions seleccionades.</string>
<string name="sftp_internal_storage">Emmagatzematge intern</string>
<string name="sftp_all_files">Tots els fitxers</string>
<string name="sftp_sdcard_num">Targeta SD %d</string>
<string name="sftp_sdcard">Targeta SD</string>
<string name="sftp_readonly">(només de lectura)</string>
<string name="sftp_camera">Imatges de la càmera</string>
<string name="add_device_dialog_title">Afegeix un dispositiu</string>
<string name="add_device_hint">Nom de la màquina o adreça IP</string>
<string name="sftp_preference_detected_sdcards">S\'han detectat targetes SD</string>
<string name="sftp_preference_edit_sdcard_title">Edita una targeta SD</string>
<string name="sftp_preference_configured_storage_locations">Ubicacions d\'emmagatzematge configurades</string>
<string name="sftp_preference_add_storage_location_title">Afegeix una ubicació d\'emmagatzematge</string>
<string name="sftp_preference_edit_storage_location">Edita una ubicació d\'emmagatzematge</string>
<string name="sftp_preference_add_camera_shortcut">Afegeix una drecera a la carpeta de càmera</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">Afegeix una drecera a la carpeta de la càmera</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">No afegeixis cap drecera a la carpeta de la càmera</string>
<string name="sftp_storage_preference_storage_location">Ubicació d\'emmagatzematge</string>
<string name="sftp_storage_preference_storage_location_already_configured">Aquesta ubicació ja s\'ha configurat</string>
<string name="sftp_storage_preference_click_to_select">clic per seleccionar</string>
<string name="sftp_storage_preference_display_name">Nom a mostrar</string>
<string name="sftp_storage_preference_display_name_already_used">Aquest nom a mostrar ja està en ús</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">El nom a mostrar no pot estar buit</string>
<string name="sftp_action_mode_menu_delete">Suprimeix</string>
<string name="sftp_no_sdcard_detected">No s\'ha detectat cap targeta SD</string>
<string name="sftp_no_storage_locations_configured">No s\'ha configurat cap ubicació d\'emmagatzematge</string>
<string name="sftp_saf_permission_explanation">Per accedir remotament als fitxer cal configurar les ubicacions d\'emmagatzematge</string>
<string name="add_host">Afegeix una màquina/IP</string>
<string name="add_host_hint">Nom de la màquina o IP</string>
<string name="no_players_connected">No s\'ha trobat cap reproductor</string>
<string name="mpris_player_on_device">%1$s sobre el %2$s</string>
<string name="send_files">Envia els fitxers</string>
@@ -209,7 +229,7 @@
<string name="findmyphone_title">Troba el meu telèfon</string>
<string name="findmyphone_title_tablet">Troba la meva tauleta</string>
<string name="findmyphone_title_tv">Troba la meva TV</string>
<string name="findmyphone_description">Fa sonar aquest dispositiu perquè el pugueu trobar.</string>
<string name="findmyphone_description">Fa sonar aquest dispositiu perquè el pugueu trobar</string>
<string name="findmyphone_found">L\'he trobat</string>
<string name="open">Obre</string>
<string name="close">Tanca</string>
@@ -218,7 +238,6 @@
<string name="permission_explanation">Aquest connector necessita permisos per a funcionar</string>
<string name="optional_permission_explanation">Us caldrà concedir permisos extres per accedir a totes les característiques</string>
<string name="plugins_need_optional_permission">Alguns connectors tenen característiques desactivades per la falta de permís (puntegeu per a més informació):</string>
<string name="sftp_permission_explanation">Per accedir als fitxers des del PC, l\'aplicació necessita permís per accedir a l\'emmagatzematge del telèfon</string>
<string name="share_optional_permission_explanation">Per a compartir fitxers entre el telèfon i l\'escriptori, haureu de donar accés a l\'emmagatzematge del telèfon</string>
<string name="telepathy_permission_explanation">Per a llegir i escriure SMS des de l\'escriptori, haureu de donar permís als SMS</string>
<string name="telephony_permission_explanation">Per a veure les trucades telefòniques i SMS des de l\'escriptori, haureu de donar permís a les trucades telefòniques i SMS</string>
@@ -273,6 +292,6 @@
<string name="block_contents">Bloca el contingut de les notificacions</string>
<string name="block_images">Bloca les imatges a les notificacions</string>
<string name="notification_channel_receivenotification">Notificacions des d\'altres dispositius</string>
<string name="take_picture">Pren una fotografia</string>
<string name="plugin_photo_desc">Pren una fotografia i l\'envia a un altre dispositiu</string>
<string name="take_picture">Llança la càmera</string>
<string name="plugin_photo_desc">Llança l\'aplicació de la càmera per facilitar la presa i la transferència de fotografies</string>
</resources>

View File

@@ -189,11 +189,12 @@
<string name="title_activity_notification_filter">Filtr upozornění</string>
<string name="filter_apps_info">Upozorňování mezi vybranými aplikacemi bude synchronizováno.</string>
<string name="sftp_internal_storage">Interní úložiště</string>
<string name="sftp_all_files">Všechny soubory</string>
<string name="sftp_sdcard_num">SD karta %d</string>
<string name="sftp_sdcard">SD karta</string>
<string name="sftp_readonly">(pouze ke čtení)</string>
<string name="sftp_camera">Obrázky z fotoaparátu</string>
<string name="add_host">Přidat stroj/IP</string>
<string name="add_host_hint">Název hostitele nebo IP</string>
<string name="no_players_connected">Přehrávač nenalezen</string>
<string name="mpris_player_on_device">%1$s na %2$s</string>
<string name="send_files">Odeslat soubory</string>
@@ -221,7 +222,6 @@
<string name="permission_explanation">Tento modul potřebuje pro práci povolení</string>
<string name="optional_permission_explanation">Pro zpřístupnění všech funkcí potřebujete další oprávnění</string>
<string name="plugins_need_optional_permission">Některé moduly mají vypnuté vlastnosti, kvůli nedostatečným oprávněním (ťukněte pro více informací):</string>
<string name="sftp_permission_explanation">Pro přístup k souborům z vašeho počítače aplikace potřebuje oprávnění k úložišti telefonu</string>
<string name="share_optional_permission_explanation">Pro sdílení souborů mezi telefonem a počítačem potřebujete udělit oprávnění k úložišti telefonu</string>
<string name="telepathy_permission_explanation">Pro čtení a psaní SMS z počítače musíte udělit oprávnění k SMS</string>
<string name="telephony_permission_explanation">Pro zobrazení telefonátů a SMS v počítači musíte udělit oprávnění k telefonování a SMS</string>

View File

@@ -139,11 +139,11 @@
<string name="title_activity_notification_filter">Bekendtgørelsesfilter</string>
<string name="filter_apps_info">Bekendtgørelser vil blive synkroniseret for de valgte apps.</string>
<string name="sftp_internal_storage">Intern lagring</string>
<string name="sftp_all_files">Alle filer</string>
<string name="sftp_sdcard_num">SD-kort %d</string>
<string name="sftp_sdcard">SD-kort</string>
<string name="sftp_readonly">(skrivebeskyttet)</string>
<string name="sftp_camera">Kamerabilleder</string>
<string name="add_host">Tilføj vært/IP</string>
<string name="no_players_connected">Ingen afspillere fundet</string>
<string name="mpris_player_on_device">%1$s på %2$s</string>
<string name="send_files">Send filer</string>
@@ -170,7 +170,6 @@
<string name="permission_explanation">Dette plugin kræver tilladelser for at virke</string>
<string name="optional_permission_explanation">Du skal give ekstra tilladelser for at aktivere alle funktioner</string>
<string name="plugins_need_optional_permission">Nogle plugins har deaktiverede funktioner pga. manglende tilladelser (tap for mere info):</string>
<string name="sftp_permission_explanation">For at tilgå filerne fra din pc, skal app\'en have tilladelse til at til gå telefonens datalager</string>
<string name="share_optional_permission_explanation">For at dele filer mellem din telefon og din desktop skal du give adgang til telefonens datalager.</string>
<string name="telepathy_permission_explanation">For at læse og skrive sms\'er fra din desktop, skal du give tilladelse til sms</string>
<string name="telephony_permission_explanation">For at se telefonopkald og sms\'er fra desktoppen, skal du give tilladelse til telefonopkald og sms.</string>

View File

@@ -150,11 +150,11 @@
<string name="title_activity_notification_filter">Benachrichtigungs-Filter</string>
<string name="filter_apps_info">Benachrichtigungen werden zwischen den ausgewählten Anwendungen abgeglichen.</string>
<string name="sftp_internal_storage">Interner Speicher</string>
<string name="sftp_all_files">Alle Dateien</string>
<string name="sftp_sdcard_num">SD-Karte %d</string>
<string name="sftp_sdcard">SD-Karte</string>
<string name="sftp_readonly">(Nur lesen)</string>
<string name="sftp_camera">Kamerabilder</string>
<string name="add_host">Rechner/IP-Adresse hinzufügen</string>
<string name="no_players_connected">Keine Medienspieler gefunden</string>
<string name="mpris_player_on_device">%1$s auf %2$s</string>
<string name="send_files">Dateien senden</string>
@@ -182,7 +182,6 @@
<string name="permission_explanation">Dieses Modul benötigt zusätzliche Berechtigungen</string>
<string name="optional_permission_explanation">Es müssen weitere Berechtigungen erteilt werden, um alle Funktionen nutzen zu können</string>
<string name="plugins_need_optional_permission">Einige Module haben eingeschränkte Funktionen wegen fehlender Berechtigungen, tippen Sie für weitere Informationen:</string>
<string name="sftp_permission_explanation">Um vom Rechner auf den Telefonspeicher zuzugreifen, werden weitere Berechtigungen benötigt</string>
<string name="share_optional_permission_explanation">m Dateien zwischen Rechner und Telefon auszutauschen, muss der Zugriff auf den Telefonspeicher gewährt werden</string>
<string name="telepathy_permission_explanation">Um SMS vom Rechner aus zu lesen und zu versenden, muss der Zugriff auf die SMS-Funktion gewährt werden</string>
<string name="telephony_permission_explanation">Um Telefonate und SMS auf dem Rechner zu sehen, müssen Berechtigungen für Anrufe und SMS erteilt werden</string>

View File

@@ -139,11 +139,11 @@
<string name="title_activity_notification_filter">Φιλτράρισμα ειδοποιήσεων</string>
<string name="filter_apps_info">Οι ειδοποιήσεις θα συγχρονίζονται για επιλεγμένες εφαρμογές.</string>
<string name="sftp_internal_storage">Εσωτερικός αποθηκευτικός χώρος</string>
<string name="sftp_all_files">Όλα τα αρχεία</string>
<string name="sftp_sdcard_num">SD card %d</string>
<string name="sftp_sdcard">SD card</string>
<string name="sftp_readonly">(ανάγνωση μόνο)</string>
<string name="sftp_camera">Φωτογραφίες</string>
<string name="add_host">Προσθήκη υπολογιστή/IP</string>
<string name="no_players_connected">Δεν βρέθηκαν συσκευές αναπαραγωγής</string>
<string name="mpris_player_on_device">%1$s σε %2$s</string>
<string name="send_files">Αποστολή αρχείων</string>
@@ -170,7 +170,6 @@
<string name="permission_explanation">Αυτό το πρόσθετο χρειάζεται δικαιώματα για να λειτουργήσει</string>
<string name="optional_permission_explanation">Απαιτείται παραχώρηση επιπλέον δικαιωμάτων για την ενεργοποίηση όλων των λειτουργιών</string>
<string name="plugins_need_optional_permission">Κάποια πρόσθετα έχουν λειτουργίες ανενεργές εξαιτίας της απουσίας δικαιωμάτων (χτυπήστε για περισσότερες πληροφορίες):</string>
<string name="sftp_permission_explanation">Για την πρόσβαση στα αρχεία σας από τον υπολογιστή η εφαρμογή χρειάζεται δικαιώματα πρόσβασης στον αποθηκευτικό χώρο του κινητού σας</string>
<string name="share_optional_permission_explanation">Για το διαμοιρασμό αρχείων ανάμεσα στο τηλέφωνο και τον υπολογιστή σας χρειάζεται να παραχωρήσετε πρόσβαση στον αποθηκευτικό χώρο του τηλεφώνου σας</string>
<string name="telepathy_permission_explanation">Για να διαβάσετε και να γράψετε SMS από την επιφάνεια εργασίας, χρειάζεται να δώσετε δικαιώματα στο SMS</string>
<string name="telephony_permission_explanation">Για να δείτε τηλεφωνικές κλήσεις και SMS από την επιφάνεια εργασίας, χρειάζεται να παραχωρήσετε δικαιώματα σε τηλεφωνικές κλήσεις και SMS</string>

View File

@@ -161,11 +161,11 @@
<string name="title_activity_notification_filter">Notification filter</string>
<string name="filter_apps_info">Notifications will be synchronised 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_host">Add host/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>
@@ -193,7 +193,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>

View File

@@ -184,13 +184,32 @@
<string name="title_activity_notification_filter">Filtro de notificaciones</string>
<string name="filter_apps_info">Las notificaciones se sincronizarán en las aplicaciones seleccionadas.</string>
<string name="sftp_internal_storage">Almacenamiento interno</string>
<string name="sftp_all_files">Todos los archivos</string>
<string name="sftp_sdcard_num">Tarjeta SD %d</string>
<string name="sftp_sdcard">Tarjeta SD</string>
<string name="sftp_readonly">(solo lectura)</string>
<string name="sftp_camera">Imágenes de la cámara</string>
<string name="add_device_dialog_title">Añadir dispositivo</string>
<string name="add_device_hint">Nombre o dirección IP</string>
<string name="sftp_preference_detected_sdcards">Tarjetas SD detectadas</string>
<string name="sftp_preference_edit_sdcard_title">Editar tarjeta SD</string>
<string name="sftp_preference_configured_storage_locations">Localizaciones de almacenamiento configuradas</string>
<string name="sftp_preference_add_storage_location_title">Añadir localización de almacenamiento</string>
<string name="sftp_preference_edit_storage_location">Editar localización de almacenamiento</string>
<string name="sftp_preference_add_camera_shortcut">Añadir acceso rápido a la carpeta de la cámara</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">Añadir un acceso rápido a la carpeta de la cámara</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">No añadir un acceso rápido a la carpeta de la cámara</string>
<string name="sftp_storage_preference_storage_location">Localización de almacenamiento</string>
<string name="sftp_storage_preference_storage_location_already_configured">Esta localización ya ha sido configurada</string>
<string name="sftp_storage_preference_click_to_select">pulsar para seleccionar</string>
<string name="sftp_storage_preference_display_name">Mostrar nombre</string>
<string name="sftp_storage_preference_display_name_already_used">Este nombre de dispositivo ya está en uso</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">El nombre de dispositivo no puede estar vacío</string>
<string name="sftp_action_mode_menu_delete">Borrar</string>
<string name="sftp_no_sdcard_detected">No se ha detectado ninguna tarjeta SD</string>
<string name="sftp_no_storage_locations_configured">No hay configurada ninguna localización de almacenamiento</string>
<string name="sftp_saf_permission_explanation">Para acceder a los archivos remotamente debe configurar las localizaciones de almacenamiento</string>
<string name="add_host">Añadir servidor/IP</string>
<string name="add_host_hint">Nombre o IP</string>
<string name="no_players_connected">Ningún reproductor encontrado</string>
<string name="mpris_player_on_device">%1$s en %2$s</string>
<string name="send_files">Enviar archivos</string>
@@ -218,7 +237,6 @@
<string name="permission_explanation">Este complemento necesita permisos para funcionar</string>
<string name="optional_permission_explanation">Debe otorgar permisos extra para activar todas las funciones</string>
<string name="plugins_need_optional_permission">Algunos complementos tienen funcionalidades desactivadas por falta de permisos (pulse para más información):</string>
<string name="sftp_permission_explanation">Para acceder a sus archivos desde su equipo, la aplicación necesita permisos para acceder al almacenamiento de su teléfono</string>
<string name="share_optional_permission_explanation">Para compartir archivos entre su teléfono y su escritorio, necesita dar acceso al almacenamiento de su teléfono</string>
<string name="telepathy_permission_explanation">Para leer y escribir SMS desde su escritorio, necesita dar permisos para SMS</string>
<string name="telephony_permission_explanation">Para ver las llamadas telefónicas y SMS desde su escritorio, necesita dar permisos para llamadas telefónicas y SMS</string>
@@ -273,4 +291,6 @@
<string name="block_contents">Bloquear el contenido de las notificaciones</string>
<string name="block_images">Bloquear las imágenes en las notificaciones</string>
<string name="notification_channel_receivenotification">Notificaciones desde otros dispositivos</string>
<string name="take_picture">Lanzar cámara</string>
<string name="plugin_photo_desc">Lanzar la aplicación de la cámara para facilitar tomar y transferir imágenes</string>
</resources>

View File

@@ -133,11 +133,11 @@
<string name="title_activity_notification_filter">Märguannete filter</string>
<string name="filter_apps_info">Valitud rakenduste märguanded sünkroonitakse</string>
<string name="sftp_internal_storage">Sisemine salvesti</string>
<string name="sftp_all_files">Kõik failid</string>
<string name="sftp_sdcard_num">SD-kaart %d</string>
<string name="sftp_sdcard">SD-kaart</string>
<string name="sftp_readonly">(kirjutuskaitstud)</string>
<string name="sftp_camera">Kaamera pildid</string>
<string name="add_host">Lisa masin/IP</string>
<string name="no_players_connected">Ühtegi mängijat ei leitud</string>
<string name="mpris_player_on_device">%1$s seadmes %2$s</string>
<string name="send_files">Saada faile</string>

View File

@@ -179,11 +179,11 @@
<string name="title_activity_notification_filter">Jakinarazpenen iragazkia</string>
<string name="filter_apps_info">Aukeratutako aplikazioen jakinarazpenak sinkronizatuko dira</string>
<string name="sftp_internal_storage">Barne biltegiratzea</string>
<string name="sftp_all_files">Fitxategi guztiak</string>
<string name="sftp_sdcard_num">%d SD txartela</string>
<string name="sftp_sdcard">SD txartela</string>
<string name="sftp_readonly">(irakurri soilik)</string>
<string name="sftp_camera">Kamerako irudiak</string>
<string name="add_host">Gehitu ostalaria/IP</string>
<string name="no_players_connected">Ez da jokalaririk aurkitu</string>
<string name="mpris_player_on_device">%1$s - %2$s</string>
<string name="send_files">Bidali fitxategiak</string>
@@ -211,7 +211,6 @@
<string name="permission_explanation">Plugin honek baimena behar du funtzionatzeko</string>
<string name="optional_permission_explanation">Baimen gehiago eman behar dituzu funtzio guztiak gaitzeko</string>
<string name="plugins_need_optional_permission">Plugin batzuek desgaitutako eginbideak dituzte baimenak faltan dituztelako (tak egin informazio gehiagorako):</string>
<string name="sftp_permission_explanation">Zure fitxategiak PCtik atzitzeko aplikazioak zure telefonoaren biltegiratzea atzitzeko baimena behar du</string>
<string name="share_optional_permission_explanation">Zure telefonoa eta mahaigainaren artean fitxategiak partekatzeko telefonoaren biltegiratzea atzitzeko baimena eman behar duzu</string>
<string name="telepathy_permission_explanation">SMSak zure mahaigainetik bidali ahal izateko, SMSak erabiltzeko baimena eman behar duzu</string>
<string name="telephony_permission_explanation">Telefono deiak eta SMSak zure mahaigainetik ikusteko, telefono deiak eta SMSak erabiltzeko baimena eman behar duzu</string>

View File

@@ -179,11 +179,11 @@
<string name="title_activity_notification_filter">Ilmoitussuodatin</string>
<string name="filter_apps_info">Valittujen sovellusten ilmoitukset synkronoidaan.</string>
<string name="sftp_internal_storage">Sisäinen muisti</string>
<string name="sftp_all_files">Kaikki tiedostot</string>
<string name="sftp_sdcard_num">SD-kortti %d</string>
<string name="sftp_sdcard">SD-kortti</string>
<string name="sftp_readonly">(vain luku)</string>
<string name="sftp_camera">Kamerakuvat</string>
<string name="add_host">Lisää kone/IP</string>
<string name="no_players_connected">Soittimia ei löytynyt</string>
<string name="mpris_player_on_device">%1$s laitteella %2$s</string>
<string name="send_files">Lähetä tiedostoja</string>
@@ -211,7 +211,6 @@
<string name="permission_explanation">Liitännäinen tarvitsee toimiakseen lisäkäyttöoikeuksia</string>
<string name="optional_permission_explanation">Kaikkien toimintojen käyttämiseksi sinun on annettava lisäkäyttöoikeuksia</string>
<string name="plugins_need_optional_permission">Jotkin liitännäisten ominaisuudet eivät ole käytössä puuttuvien käyttöoikeuksien takia (lisätietoa napsauttamalla):</string>
<string name="sftp_permission_explanation">Sovellus tarvitsee puhelimen tallennustilan käyttöoikeudet voidakseen käyttää tietokoneesi tiedostoja</string>
<string name="share_optional_permission_explanation">Jakaaksesi tiedostoja puhelimen ja työpöydän välillä sinun on annettava käyttöoikeudet puhelimen tallennustilaan</string>
<string name="telepathy_permission_explanation">Lukeaksesi ja lähettääksesi tekstiviestejä työpöydältä sinun on annettava käyttöoikeudet tekstiviesteihin</string>
<string name="telephony_permission_explanation">Nähdäksesi soitot ja tekstiviestit työpöydältä sinun on annettava käyttöoikeudet puheluihin ja tekstiviesteihin</string>

View File

@@ -179,11 +179,12 @@
<string name="title_activity_notification_filter">Filtre des notifications</string>
<string name="filter_apps_info">Les notifications seront synchronisées pour les applications sélectionnées.</string>
<string name="sftp_internal_storage">Stockage interne</string>
<string name="sftp_all_files">Tous les fichiers</string>
<string name="sftp_sdcard_num">Carte SD %d</string>
<string name="sftp_sdcard">Carte SD</string>
<string name="sftp_readonly">(lecture seule)</string>
<string name="sftp_camera">Images de l\'appareil photo</string>
<string name="add_host">Ajouter hôte/IP</string>
<string name="add_host_hint">"Nom d\'hôte ou adresse IP "</string>
<string name="no_players_connected">Aucun lecteur trouvé</string>
<string name="mpris_player_on_device">%1$s sur %2$s</string>
<string name="send_files">Envoyer des fichiers</string>
@@ -211,7 +212,6 @@
<string name="permission_explanation">Ce module externe nécessite des permissions pour fonctionner</string>
<string name="optional_permission_explanation">Vous devez accorder des permissions supplémentaires pour activer toutes les fonctionnalités</string>
<string name="plugins_need_optional_permission">Certaines fonctionnalités de modules externes sont désactivées faute de permissions suffisantes (tapez pour plus d\'informations) :</string>
<string name="sftp_permission_explanation">Pour accéder aux fichiers de votre ordinateur, l\'application requiert la permission d\'accéder à la mémoire de stockage de votre téléphone</string>
<string name="share_optional_permission_explanation">Pour partager des fichiers entre votre téléphone et votre ordinateur, veuillez permettre l\'accès à la mémoire de stockage du téléphone</string>
<string name="telepathy_permission_explanation">Pour lire et écrire des SMS depuis votre ordinateur, veuillez permettre l\'accès aux SMS</string>
<string name="telephony_permission_explanation">Pour voir les appels et les SMS depuis votre ordinateur, veuillez permettre l\'accès aux appels et aux SMS</string>

View File

@@ -24,20 +24,21 @@
<string name="pref_plugin_contacts">Sincronizador de contactos</string>
<string name="pref_plugin_contacts_desc">Permitir sincronizar o caderno de contactos do dispositivo</string>
<string name="pref_plugin_ping">Ping</string>
<string name="pref_plugin_ping_desc">Envíe e reciba pings.</string>
<string name="pref_plugin_ping_desc">Envíe e reciba pings</string>
<string name="pref_plugin_notifications">Sincronización de notificacións</string>
<string name="pref_plugin_notifications_desc">Acceda ás súas notificacións desde outros dispositivos.</string>
<string name="pref_plugin_notifications_desc">Acceda ás súas notificacións desde outros dispositivos</string>
<string name="pref_plugin_receive_notifications">Recibir notificacións</string>
<string name="pref_plugin_receive_notifications_desc">Recibir notificacións do outro dispositivo e mostralas en Android.</string>
<string name="pref_plugin_receive_notifications_desc">Recibir notificacións do outro dispositivo e mostralas en Android</string>
<string name="pref_plugin_sharereceiver">Compartir e recibir</string>
<string name="pref_plugin_sharereceiver_desc">Comparta ficheiros e enderezos URL entre dispositivos.</string>
<string name="plugin_not_available">Esta funcionalidade non está dispoñíbel para a súa versión de Android.</string>
<string name="pref_plugin_sharereceiver_desc">Comparta ficheiros e enderezos URL entre dispositivos</string>
<string name="plugin_not_available">Esta funcionalidade non está dispoñíbel para a súa versión de Android</string>
<string name="device_list_empty">Non hai dispositivos.</string>
<string name="ok">Aceptar</string>
<string name="cancel">Cancelar</string>
<string name="open_settings">Abrir a configuración</string>
<string name="no_permissions">Debe conceder permisos para acceder ás notificacións.</string>
<string name="no_permission_mprisreceiver">Para poder controlar os seus reprodutores de son e vídeo ten que garantir acceso ás notificacións.</string>
<string name="no_permissions">Debe conceder permisos para acceder ás notificacións</string>
<string name="no_permission_mprisreceiver">Para poder controlar os seus reprodutores de son e vídeo ten que garantir acceso ás notificacións</string>
<string name="no_permissions_remotekeyboard">Para recibir presións de tecla ten que activar o teclado remoto de KDE Connect</string>
<string name="send_ping">Enviar un ping</string>
<string name="open_mpris_controls">Control multimedia</string>
<string name="remotekeyboard_editing_only_title">Xestionar teclas remotas só ao editar.</string>
@@ -184,13 +185,32 @@
<string name="title_activity_notification_filter">Filtro de notificacións</string>
<string name="filter_apps_info">As notificacións sincronizaranse para os seguintes aplicativos.</string>
<string name="sftp_internal_storage">Almacenamento interno</string>
<string name="sftp_all_files">Todos os ficheiros</string>
<string name="sftp_sdcard_num">Tarxeta SD %d</string>
<string name="sftp_sdcard">Tarxeta SD</string>
<string name="sftp_readonly">(só lectura)</string>
<string name="sftp_camera">Imaxes da cámara</string>
<string name="add_device_dialog_title">Engadir o dispositivo</string>
<string name="add_device_hint">Nome de máquina ou enderezo IP</string>
<string name="sftp_preference_detected_sdcards">Tarxetas SD detectadas</string>
<string name="sftp_preference_edit_sdcard_title">Editar a tarxeta SD</string>
<string name="sftp_preference_configured_storage_locations">Lugares de almacenamento configurados</string>
<string name="sftp_preference_add_storage_location_title">Engadir un lugar de almacenamento</string>
<string name="sftp_preference_edit_storage_location">Editar un lugar de almacenamento</string>
<string name="sftp_preference_add_camera_shortcut">Engadir un atallo ao cartafol de cámara</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">Engadir un atallo ao cartafol da cámara</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">Non engadir un atallo ao cartafol da cámara</string>
<string name="sftp_storage_preference_storage_location">Lugar de almacenamento</string>
<string name="sftp_storage_preference_storage_location_already_configured">Este lugar xa está configurado</string>
<string name="sftp_storage_preference_click_to_select">premer para seleccionar</string>
<string name="sftp_storage_preference_display_name">Nome para mostrar</string>
<string name="sftp_storage_preference_display_name_already_used">Este nome para mostrar xa está a usarse</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">O nome para mostrar non pode estar baleiro</string>
<string name="sftp_action_mode_menu_delete">Eliminar</string>
<string name="sftp_no_sdcard_detected">Non se detectaron tarxetas SD</string>
<string name="sftp_no_storage_locations_configured">Non se configuraron localizacións de almacenamento</string>
<string name="sftp_saf_permission_explanation">Para acceder a ficheiro remotamente ten que configurar lugares de almacenamento</string>
<string name="add_host">Engadir unha nome ou IP</string>
<string name="add_host_hint">Nome de máquina ou IP</string>
<string name="no_players_connected">Non se atoparon reprodutores.</string>
<string name="mpris_player_on_device">%1$s en %2$s</string>
<string name="send_files">Enviar ficheiros</string>
@@ -204,7 +224,7 @@
<string name="on_data_message">Parece que está usando unha conexión de datos de móbil. KDE Connect só funciona en redes locais.</string>
<string name="no_file_browser">Non hai navegadores de ficheiros instalados.</string>
<string name="pref_plugin_telepathy">Enviar unha mensaxe de texto</string>
<string name="pref_plugin_telepathy_desc">Enviar mensaxes de texto desde un computador de escritorio.</string>
<string name="pref_plugin_telepathy_desc">Enviar mensaxes de texto desde o seu escritorio</string>
<string name="plugin_not_supported">O dispositivo non é compatíbel con este complemento.</string>
<string name="findmyphone_title">Atopar o móbil</string>
<string name="findmyphone_title_tablet">Atopar a tableta</string>
@@ -218,7 +238,6 @@
<string name="permission_explanation">Este complemento necesita permisos para funcionar.</string>
<string name="optional_permission_explanation">Ten que conceder permisos adicionais para activar todas as funcións.</string>
<string name="plugins_need_optional_permission">Algúns complementos teñen funcionalidades desactivadas por mor dunha falta de permisos (toque para máis información):</string>
<string name="sftp_permission_explanation">Para acceder aos seus ficheiros desde o computador o aplicativo necesita permiso para acceder ao almacenamento do teléfono.</string>
<string name="share_optional_permission_explanation">Para compartir ficheiros entre o teléfono e o escritorio ten que dar acceso ao almacenamento do teléfono.</string>
<string name="telepathy_permission_explanation">Para ler e escribir SMS desde o escritorio ten que dar permiso de SMS.</string>
<string name="telephony_permission_explanation">Para ver as chamadas de teléfono e os SMS desde o escritorio ten que dar permiso a chamadas de teléfono e a SMS.</string>
@@ -273,6 +292,6 @@
<string name="block_contents">Bloquear o contido das notificacións</string>
<string name="block_images">Bloquear as imaxes nas notificacións</string>
<string name="notification_channel_receivenotification">Notificacións desde outros dispositivos</string>
<string name="take_picture">Sacar unha foto</string>
<string name="plugin_photo_desc">Sacar unha foto e enviala a outro dispositivo</string>
<string name="take_picture">Iniciar a cámara</string>
<string name="plugin_photo_desc">Iniciar o aplicativo da cámara para facilitar sacar e transferir imaxes</string>
</resources>

View File

@@ -139,11 +139,11 @@
<string name="title_activity_notification_filter">סנן התראות</string>
<string name="filter_apps_info">התראות יסונכרנו רק לאפליקציות נבחרות</string>
<string name="sftp_internal_storage">זיכרון פנימי</string>
<string name="sftp_all_files">כל הקבצים</string>
<string name="sftp_sdcard_num">כרטיס זיכרון %d</string>
<string name="sftp_sdcard">כרטיס זיכרון</string>
<string name="sftp_readonly">(לקריאה בלבד)</string>
<string name="sftp_camera">תמונות מצלמה</string>
<string name="add_host">הוסף כתובת או IP</string>
<string name="no_players_connected">לא נמצא נגן</string>
<string name="mpris_player_on_device">%1$s אצל %2$s</string>
<string name="send_files">שלח קובץ</string>

View File

@@ -172,11 +172,11 @@
<string name="title_activity_notification_filter">Filter notifikasi</string>
<string name="filter_apps_info">Notifikasi akan disinkronkan terhadap apl terpilih.</string>
<string name="sftp_internal_storage">Penyimpanan internal</string>
<string name="sftp_all_files">Semua file</string>
<string name="sftp_sdcard_num">Kartu SD %d</string>
<string name="sftp_sdcard">Kartu SD</string>
<string name="sftp_readonly">(hanya baca)</string>
<string name="sftp_camera">Gambar kamera</string>
<string name="add_host">Tambahkan host/IP</string>
<string name="no_players_connected">Tidak ada player yang ditemukan</string>
<string name="mpris_player_on_device">%1$s pada %2$s</string>
<string name="send_files">Kirim file</string>
@@ -204,7 +204,6 @@
<string name="permission_explanation">Plugin ini perlu perizinan untuk kerja</string>
<string name="optional_permission_explanation">Kamu perlu mengabulkan perizinan extra untuk memfungsikan semua fungsian</string>
<string name="plugins_need_optional_permission">Beberapa plugin yang memiliki fitur dinonfungsikan karena kurangnya perizinan (ketuk untuk info selebihnya):</string>
<string name="sftp_permission_explanation">Untuk mengakses filemu dari PC-mu si apl perlu perizinan untuk mengakses penyimpanan teleponmu</string>
<string name="share_optional_permission_explanation">Untuk membagikan file antara teleponmu dan desktopmu kamu harus memberikan akses ke penyimpanan teleponmu</string>
<string name="telepathy_permission_explanation">Untuk membaca dan menulis SMS dari desktopmu kamu harus memberikan perizinan untuk SMS</string>
<string name="telephony_permission_explanation">Untuk melihat paggian telepon dan SMS dari desktopmu kamu harus memberikan perizinan untuk panggilan telepon dan SMS</string>

View File

@@ -184,13 +184,13 @@
<string name="title_activity_notification_filter">Filtro delle notifiche</string>
<string name="filter_apps_info">Le notifiche saranno sincronizzate per le applicazioni selezionate.</string>
<string name="sftp_internal_storage">Archiviazione interna</string>
<string name="sftp_all_files">Tutti i file</string>
<string name="sftp_sdcard_num">Scheda SD %d</string>
<string name="sftp_sdcard">Scheda SD</string>
<string name="sftp_readonly">(sola lettura)</string>
<string name="sftp_camera">Immagini fotocamera</string>
<string name="add_device_dialog_title">Aggiungi dispositivo</string>
<string name="add_device_hint">Nome host o indirizzo IP</string>
<string name="add_host">Aggiungi host/IP</string>
<string name="no_players_connected">Nessun lettore trovato</string>
<string name="mpris_player_on_device">%1$s su %2$s</string>
<string name="send_files">Invia file</string>
@@ -218,7 +218,6 @@
<string name="permission_explanation">Questa estensione ha bisogno di permessi per funzionare</string>
<string name="optional_permission_explanation">Devi concedere permessi aggiuntivi per abilitare tutte le funzioni</string>
<string name="plugins_need_optional_permission">Alcune estensioni hanno funzioni disabilitate per una mancanza di permessi (tocca per maggiori informazioni):</string>
<string name="sftp_permission_explanation">Per accedere ai tuoi file dal tuo PC, l\'applicazione ha bisogno dell\'autorizzazione di accesso alla memoria del telefono</string>
<string name="share_optional_permission_explanation">Per condividere i file tra il telefono e il tuo desktop devi dare accesso alla memoria del telefono</string>
<string name="telepathy_permission_explanation">Per leggere e scrivere SMS dal tuo desktop, devi concedere l\'autorizzazione per SMS</string>
<string name="telephony_permission_explanation">Per vedere le chiamate telefoniche e gli SMS dal desktop devi dare l\'autorizzazione per telefonate e SMS</string>

View File

@@ -152,11 +152,11 @@
<string name="title_activity_notification_filter">알림 필터</string>
<string name="filter_apps_info">선택한 앱의 알림을 동기화합니다.</string>
<string name="sftp_internal_storage">내부 저장소</string>
<string name="sftp_all_files">모든 파일</string>
<string name="sftp_sdcard_num">SD 카드 %d</string>
<string name="sftp_sdcard">SD 카드</string>
<string name="sftp_readonly">(읽기 전용)</string>
<string name="sftp_camera">카메라 사진</string>
<string name="add_host">호스트/IP 주소 추가</string>
<string name="no_players_connected">재생기를 찾을 수 없음</string>
<string name="mpris_player_on_device">%2$s의 %1$s</string>
<string name="send_files">파일 보내기</string>
@@ -184,7 +184,6 @@
<string name="permission_explanation">이 플러그인을 사용하려면 권한이 필요합니다</string>
<string name="optional_permission_explanation">모든 기능을 사용하려면 추가 권한이 필요합니다</string>
<string name="plugins_need_optional_permission">일부 플러그인은 권한이 없어서 비활성화되었습니다(정보를 보려면 누르기):</string>
<string name="sftp_permission_explanation">PC에서 파일에 접근하려면 앱에서 휴대폰 저장소 접근 권한이 필요합니다</string>
<string name="share_optional_permission_explanation">휴대폰과 데스크톱간 파일을 공유하려면 휴대폰 저장소 접근 권한이 필요합니다</string>
<string name="telepathy_permission_explanation">데스크톱에서 문자 메시지를 읽고 보내려면 문자 메시지 접근 권한이 필요합니다</string>
<string name="telephony_permission_explanation">데스크톱에서 통화와 문자 메시지를 보려면 통화 및 문자 메시지 접근 권한이 필요합니다</string>

View File

@@ -87,11 +87,11 @@
<string name="pair_device_action">Suporuoti naują įrenginį</string>
<string name="unpair_device_action">Atrišti %s</string>
<string name="custom_device_list">Pridėti įrenginį pagal IP</string>
<string name="sftp_all_files">Visi failai</string>
<string name="sftp_sdcard_num">SD kortelė %d</string>
<string name="sftp_sdcard">SD kortelė</string>
<string name="sftp_readonly">(tik skaitymui)</string>
<string name="sftp_camera">Nuotraukos</string>
<string name="add_host">Pridėti kompiuterį / IP</string>
<string name="mpris_player_on_device">%1$s - %2$s</string>
<string name="send_files">Siųsti failus</string>
<string name="pairing_title">„KDE Connect“ įrenginiai</string>

View File

@@ -38,6 +38,7 @@
<string name="open_settings">Instellingen openen</string>
<string name="no_permissions">U moet toestemming geven voor toegang tot meldingen</string>
<string name="no_permission_mprisreceiver">Om in staat te zijn uw mediaspelers te besturen moet u toegan geven tot de meldingen</string>
<string name="no_permissions_remotekeyboard">Om indrukken van toetsen te ontvangen moet u het KDE Connect Remote Keyboard activeren</string>
<string name="send_ping">Ping verzenden</string>
<string name="open_mpris_controls">Bediening van multimedia</string>
<string name="remotekeyboard_editing_only_title">Behandel toetsen op afstand alleen bij bewerken</string>
@@ -184,13 +185,32 @@
<string name="title_activity_notification_filter">Filter voor meldingen</string>
<string name="filter_apps_info">Meldingen zullen gesynchroniseerd worden voor de geselecteerde apps.</string>
<string name="sftp_internal_storage">Interne opslag</string>
<string name="sftp_all_files">Alle bestanden</string>
<string name="sftp_sdcard_num">SD-kaartje %d</string>
<string name="sftp_sdcard">SD-kaartje</string>
<string name="sftp_readonly">(alleen-lezen)</string>
<string name="sftp_camera">Afbeeldingen van camera</string>
<string name="add_device_dialog_title">Apparaat toevoegen</string>
<string name="add_device_hint">Hostnaam of IP-adres</string>
<string name="sftp_preference_detected_sdcards">SD-kaarten gevonden</string>
<string name="sftp_preference_edit_sdcard_title">SD-kaart bewerken</string>
<string name="sftp_preference_configured_storage_locations">Opslaglocaties geconfigureerd</string>
<string name="sftp_preference_add_storage_location_title">Opslaglocatie toegevoegd</string>
<string name="sftp_preference_edit_storage_location">Opslaglocatie bewerken</string>
<string name="sftp_preference_add_camera_shortcut">Sneltoets voor cameramap toevoegen</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">Een sneltoets naar de cameramap toevoegen</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">Geen sneltoets naar de cameramap toevoegen</string>
<string name="sftp_storage_preference_storage_location">Opslaglocatie</string>
<string name="sftp_storage_preference_storage_location_already_configured">Deze locatie is al geconfigureerd</string>
<string name="sftp_storage_preference_click_to_select">klik om te selecteren</string>
<string name="sftp_storage_preference_display_name">Schermnaam</string>
<string name="sftp_storage_preference_display_name_already_used">Deze schermnaam wordt al gebruikt</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">Schermnaam mag niet leeg zijn</string>
<string name="sftp_action_mode_menu_delete">Verwijderen</string>
<string name="sftp_no_sdcard_detected">Geen SD-kaart gedetecteerd</string>
<string name="sftp_no_storage_locations_configured">Geen opslaglocaties geconfigureerd</string>
<string name="sftp_saf_permission_explanation">Om toegang te hebben tot bestanden op afstand moet u opslaglocaties configureren</string>
<string name="add_host">Host/IP-adres toevoegen</string>
<string name="add_host_hint">Hostnaam of IP-adres</string>
<string name="no_players_connected">Geen spelers gevonden</string>
<string name="mpris_player_on_device">%1$s op %2$s</string>
<string name="send_files">Bestanden verzenden</string>
@@ -218,7 +238,6 @@
<string name="permission_explanation">Deze plug-in heeft toestemming nodig om te werken</string>
<string name="optional_permission_explanation">U moet toestemming geven om alle functies in te schakelen</string>
<string name="plugins_need_optional_permission">Sommige plug-ins hebben functies uitgeschakeld vanwege ontbrekende toestemming (tik voor meer informatie):</string>
<string name="sftp_permission_explanation">"Om toegang tot uw bestanden te krijgen vanuit uw PC heeft de app toestemming nodig voor toegang tot de opslag van uw telefoon "</string>
<string name="share_optional_permission_explanation">Om bestanden tussen uw telefoon en uw bureaublad te delen moet u toegang geven tot de opslag van uw telefoon</string>
<string name="telepathy_permission_explanation">Om een SMS te lezen of te schrijven vanaf uw bureaublad moet u toestemming geven tot SMS</string>
<string name="telephony_permission_explanation">Om telefoonoproepen en SMS te zien vanaf het bureaublad moet u toestemming geven tot telefoonoproepen en SMS</string>
@@ -273,6 +292,6 @@
<string name="block_contents">Inhoud van meldingen blokkeren</string>
<string name="block_images">Afbeeldingen in meldingen blokkeren</string>
<string name="notification_channel_receivenotification">Meldingen van andere apparaten</string>
<string name="take_picture">Foto nemen</string>
<string name="plugin_photo_desc">Neem een foto en stuur het naar een ander apparaat</string>
<string name="take_picture">Start camera</string>
<string name="plugin_photo_desc">Start de camera-app om nemen en overdragen van afbeeldingen te vergemakkelijken</string>
</resources>

View File

@@ -179,11 +179,11 @@
<string name="title_activity_notification_filter">Varslingsfilter</string>
<string name="filter_apps_info">Varslingar vert synkroniserte for dei valde appane.</string>
<string name="sftp_internal_storage">Intern lagring</string>
<string name="sftp_all_files">Alle filer</string>
<string name="sftp_sdcard_num">SD-kort %d</string>
<string name="sftp_sdcard">SD-kort</string>
<string name="sftp_readonly">(skriveverna)</string>
<string name="sftp_camera">Kamerabilete</string>
<string name="add_host">Legg til vert/IP</string>
<string name="no_players_connected">Fann ingen spelarar</string>
<string name="mpris_player_on_device">%1$s på %2$s</string>
<string name="send_files">Send filer</string>
@@ -211,7 +211,6 @@
<string name="permission_explanation">Dette tillegget treng utvida løyve for å fungera</string>
<string name="optional_permission_explanation">Du må gje utvida løyve for at alle funksjonane skal fungera</string>
<string name="plugins_need_optional_permission">På grunn av manglande løyve har nokre av tillegga funksjonar slåtte av (trykk på dei for meir informasjon):</string>
<string name="sftp_permission_explanation">For å gje tilgang til filene frå datamaskina treng appen leseløyve til lagringsområdet på telefonen</string>
<string name="share_optional_permission_explanation">For å kunna dela filer mellom telefonen og datamaskina må du gje appen lese- og skriveløyve til lagringsområdet på telefonen</string>
<string name="telepathy_permission_explanation">For å kunna lesa og skriva tekstmeldingar frå datamaskina må du gje appen tilgang til SMS</string>
<string name="telephony_permission_explanation">For å kunna sjå telefonsamtalar og tekstmeldingar frå datamaskina må du gje appen tilgang til telefon- og SMS-funksjonar</string>

View File

@@ -189,11 +189,11 @@
<string name="title_activity_notification_filter">Filtr powiadomień</string>
<string name="filter_apps_info">Powiadomienia zostaną zsynchronizowane z wybranymi aplikacjami.</string>
<string name="sftp_internal_storage">"Pamięć wewnętrzna "</string>
<string name="sftp_all_files">Wszystkie pliki</string>
<string name="sftp_sdcard_num">Karta SD %d</string>
<string name="sftp_sdcard">Karta SD</string>
<string name="sftp_readonly">(tylko do odczytu)</string>
<string name="sftp_camera">Zdjęcia z aparatu</string>
<string name="add_host">Dodaj gospodarza/IP</string>
<string name="no_players_connected">Nie znaleziono żadnego odtwarzacza</string>
<string name="mpris_player_on_device">%1$s na %2$s</string>
<string name="send_files">Wyślij pliki</string>
@@ -221,7 +221,6 @@
<string name="permission_explanation">Ta wtyczka wymaga uprawnień do działania</string>
<string name="optional_permission_explanation">Musisz przydzielić dodatkowe uprawnienia, aby włączyć wszystkie funkcje</string>
<string name="plugins_need_optional_permission">Niektóre z wtyczek mają ograniczone możliwości ze względu na ograniczone uprawnienia (stuknij po więcej informacji)</string>
<string name="sftp_permission_explanation">Aby uzyskać dostęp do plików z twojego PC aplikacja ta potrzebuje uprawnień do dostępu do pamięci twojego telefonu</string>
<string name="share_optional_permission_explanation">Aby udostępniać pliki z twojego telefonu na twoim komputerze musisz pozowolić na dostęp do pamięci telefonu</string>
<string name="telepathy_permission_explanation">Aby odczytywać i pisać SMSy z twojego komputera musisz nadać uprawnienia do SMSów</string>
<string name="telephony_permission_explanation">Aby widzieć rozmowy telefoniczne i SMSy z twojego komputera musisz nadać uprawnienia na rozmowy telefoniczne i SMSy</string>

View File

@@ -184,13 +184,32 @@
<string name="title_activity_notification_filter">Filtro de notificações</string>
<string name="filter_apps_info">As notificações dos aplicativos selecionados serão sincronizadas.</string>
<string name="sftp_internal_storage">Armazenamento interno</string>
<string name="sftp_all_files">Todos os arquivos</string>
<string name="sftp_sdcard_num">Cartão SD %d</string>
<string name="sftp_sdcard">Cartão SD</string>
<string name="sftp_readonly">(somente leitura)</string>
<string name="sftp_camera">Imagens da câmera</string>
<string name="add_device_dialog_title">Adicionar dispositivo</string>
<string name="add_device_hint">Nome da máquina ou endereço IP</string>
<string name="sftp_preference_detected_sdcards">Cartões SD detectados</string>
<string name="sftp_preference_edit_sdcard_title">Editar cartão SD</string>
<string name="sftp_preference_configured_storage_locations">Localizações de armazenamento configuradas</string>
<string name="sftp_preference_add_storage_location_title">Adicionar localização de armazenamento</string>
<string name="sftp_preference_edit_storage_location">Editar localização de armazenamento</string>
<string name="sftp_preference_add_camera_shortcut">Adicionar atalho para pasta da câmera</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">Adiciona um atalho para a pasta da câmera</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">Não adiciona um atalho para a pasta da câmera</string>
<string name="sftp_storage_preference_storage_location">Localização do armazenamento</string>
<string name="sftp_storage_preference_storage_location_already_configured">Esta localização já foi configurada</string>
<string name="sftp_storage_preference_click_to_select">clique para selecionar</string>
<string name="sftp_storage_preference_display_name">Nome de exibição</string>
<string name="sftp_storage_preference_display_name_already_used">Este nome de exibição já está em uso</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">O nome de exibição não pode esta vazio</string>
<string name="sftp_action_mode_menu_delete">Excluir</string>
<string name="sftp_no_sdcard_detected">Nenhum cartão SD detectado</string>
<string name="sftp_no_storage_locations_configured">Nenhuma localização de armazenamento configurada</string>
<string name="sftp_saf_permission_explanation">Para acessar arquivos remotamente você precisa configurar localizações de armazenamento</string>
<string name="add_host">Adicionar máquina/IP</string>
<string name="add_host_hint">Nome da máquina ou IP</string>
<string name="no_players_connected">Nenhum reprodutor encontrado</string>
<string name="mpris_player_on_device">%1$s em %2$s</string>
<string name="send_files">Enviar arquivos</string>
@@ -218,7 +237,6 @@
<string name="permission_explanation">Este plugin precisa de permissões para funcionar</string>
<string name="optional_permission_explanation">Você precisa conceder permissões extras para ativar todas as funções</string>
<string name="plugins_need_optional_permission">Alguns plugins possuem recursos desativados devido à falta de permissões (toque para obter mais informações):</string>
<string name="sftp_permission_explanation">Para acessar os seus arquivos a partir do PC, o aplicativo precisa de permissão para acessar o armazenamento do seu celular</string>
<string name="share_optional_permission_explanation">Para compartilhar arquivos entre o seu celular e o seu ambiente de trabalho é necessário permissão para acessar o armazenamento do seu celular</string>
<string name="telepathy_permission_explanation">Para ler e gravar SMS a partir do seu ambiente de trabalho é necessário conceder permissão para SMS</string>
<string name="telephony_permission_explanation">Para ver as chamadas e SMS do celular a partir do seu ambiente de trabalho é necessário conceder permissão para as chamadas telefônicas e SMS</string>
@@ -273,6 +291,6 @@
<string name="block_contents">Bloquear o conteúdo das notificações</string>
<string name="block_images">Bloquear as imagens das notificações</string>
<string name="notification_channel_receivenotification">Notificações dos outros dispositivos</string>
<string name="take_picture">Tirar uma foto</string>
<string name="plugin_photo_desc">Tira uma foto e envia ela para outro dispositivo</string>
<string name="take_picture">Iniciar câmera</string>
<string name="plugin_photo_desc">Iniciar o aplicativo da câmera para facilitar a captura e transferência de fotos</string>
</resources>

View File

@@ -184,7 +184,6 @@
<string name="title_activity_notification_filter">Filtro de notificações</string>
<string name="filter_apps_info">As notificações serão sincronizadas para as aplicações seleccionadas.</string>
<string name="sftp_internal_storage">Armazenamento interno</string>
<string name="sftp_all_files">Todos os ficheiros</string>
<string name="sftp_sdcard_num">Cartão SD %d</string>
<string name="sftp_sdcard">Cartão SD</string>
<string name="sftp_readonly">(apenas para leitura)</string>
@@ -218,7 +217,6 @@
<string name="permission_explanation">Este \'plugin\' precisa de permissões para funcionar</string>
<string name="optional_permission_explanation">Precisa de dar permissões extra para activar todas as funcionalidades</string>
<string name="plugins_need_optional_permission">Alguns \'plugins\' têm funcionalidades desactivadas devido à falta de permissões (toque para obter mais informações):</string>
<string name="sftp_permission_explanation">Para aceder aos seus ficheiros a partir do seu PC, a aplicação precisa de permissão para aceder ao armazenamento do seu telemóvel</string>
<string name="share_optional_permission_explanation">Para partilhar ficheiros entre o seu telemóvel e o seu ambiente de trabalho, precisa de permissão para aceder ao armazenamento do seu telemóvel</string>
<string name="telepathy_permission_explanation">Para ler e escrever SMS\'s a partir do seu ambiente de trabalho, precisa de dar permissões para os SMS\'s</string>
<string name="telephony_permission_explanation">Para ver as chamadas e os SMS\'s a partir do seu ambiente de trabalho, precisa de dar permissões para as chamadas telefónicas e SMS\'s</string>

View File

@@ -158,11 +158,11 @@
<string name="title_activity_notification_filter">Фильтр уведомлений</string>
<string name="filter_apps_info">Уведомления будут синхронизированы для выбранных приложений.</string>
<string name="sftp_internal_storage">Встроенная память</string>
<string name="sftp_all_files">Все файлы</string>
<string name="sftp_sdcard_num">SD-карта %d</string>
<string name="sftp_sdcard">SD-карта</string>
<string name="sftp_readonly">(только чтение)</string>
<string name="sftp_camera">Фотографии с камеры</string>
<string name="add_host">Добавить хост/IP-адрес</string>
<string name="no_players_connected">Медиапроигрывателей не найдено</string>
<string name="mpris_player_on_device">%1$s на %2$s</string>
<string name="send_files">Отправить файлы</string>
@@ -189,7 +189,6 @@
<string name="permission_explanation">Этому модулю нужны разрешения для работы</string>
<string name="optional_permission_explanation">Необходимо предоставить дополнительные разрешения для включения всех функций</string>
<string name="plugins_need_optional_permission">Некоторые функции модулей отключены из-за отсутствия необходимых разрешений (нажмите для просмотра подробностей):</string>
<string name="sftp_permission_explanation">Для доступа к файлам с вашего компьютера приложению необходимо разрешение на доступ к встроенной памяти телефона</string>
<string name="share_optional_permission_explanation">Чтобы обмениваться файлами между телефоном и компьютером, необходимо предоставить доступ к встроенной памяти телефона</string>
<string name="telepathy_permission_explanation">Чтобы читать и писать SMS с компьютера, вам необходимо дать разрешение на доступ к SMS</string>
<string name="telephony_permission_explanation">Чтобы видеть телефонные звонки и SMS на компьютере, необходимо дать разрешение на телефонные звонки и SMS</string>

View File

@@ -189,11 +189,11 @@
<string name="title_activity_notification_filter">Filter upozornení</string>
<string name="filter_apps_info">Upozornenia budú synchronizované pre vybrané aplikácie.</string>
<string name="sftp_internal_storage">Interné úložisko</string>
<string name="sftp_all_files">Všetky súbory</string>
<string name="sftp_sdcard_num">SD karta %d</string>
<string name="sftp_sdcard">SD karta</string>
<string name="sftp_readonly">(iba na čítanie)</string>
<string name="sftp_camera">Obrázky fotoaparátu</string>
<string name="add_host">Pridať hostiteľa/IP</string>
<string name="no_players_connected">Nenašli sa žiadne prehrávače</string>
<string name="mpris_player_on_device">%1$s na %2$s</string>
<string name="send_files">Odoslať súbory</string>
@@ -221,7 +221,6 @@
<string name="permission_explanation">Tento plugin potrebuje oprávnenia aby fungoval</string>
<string name="optional_permission_explanation">Musíte povoliť oprávnenia na povolenie všetkých funkcií</string>
<string name="plugins_need_optional_permission">Niektoré pluginy majú zakázané funkcie pre nedostatok opránení (ťuknite pre viac info):</string>
<string name="sftp_permission_explanation">Na prístup k vaším súborom z PC, aplikácia potrebuje oprávnenie na prístup k vašemu úložisku</string>
<string name="share_optional_permission_explanation">Na zdieľanie súborov medzi vašim telefónom a počítačom potrebujete dať prístup k úložisku telefónu</string>
<string name="telepathy_permission_explanation">Na čítanie a písanie SMS z vašeho počítača, potrebujete dať oprávnienie na SMS</string>
<string name="telephony_permission_explanation">Aby ste videli telefónne hovory a SMS z počítača, potrebujete dať oprávnenie na hovory a SMS</string>

View File

@@ -38,6 +38,7 @@
<string name="open_settings">Öppna inställningarna</string>
<string name="no_permissions">Du måste ge rättighet att komma åt underrättelser</string>
<string name="no_permission_mprisreceiver">För att kunna styra mediaspelare måste du ge tillgång till underrättelser</string>
<string name="no_permissions_remotekeyboard">KDE-ansluts fjärrtangentbord måste aktiveras för att ta emot tangentnedtryckningar</string>
<string name="send_ping">Skicka ping</string>
<string name="open_mpris_controls">Kontroll av multimedia</string>
<string name="remotekeyboard_editing_only_title">Hantera bara externa tangenter vid redigering</string>
@@ -184,13 +185,32 @@
<string name="title_activity_notification_filter">Underrättelsefilter</string>
<string name="filter_apps_info">Underrättelser synkroniseras för markerade applikationer.</string>
<string name="sftp_internal_storage">Intern lagring</string>
<string name="sftp_all_files">Alla filer</string>
<string name="sftp_sdcard_num">SD-kort %d</string>
<string name="sftp_sdcard">SD-kort</string>
<string name="sftp_readonly">(skrivskyddat)</string>
<string name="sftp_camera">Kamerabilder</string>
<string name="add_device_dialog_title">Lägg till apparat</string>
<string name="add_device_hint">Värddatornamn eller IP-adress</string>
<string name="sftp_preference_detected_sdcards">Detekterade SD-kort</string>
<string name="sftp_preference_edit_sdcard_title">Redigera SD-kort</string>
<string name="sftp_preference_configured_storage_locations">Anpassa lagringsplatser</string>
<string name="sftp_preference_add_storage_location_title">Lägg till lagringsplats</string>
<string name="sftp_preference_edit_storage_location">Redigera lagringsplats</string>
<string name="sftp_preference_add_camera_shortcut">Lägg till genväg till kamerakatalog</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">Lägg till en genväg till kamerakatalogen</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">Lägg inte till en genväg till kamerakatalogen</string>
<string name="sftp_storage_preference_storage_location">Lagringsplats</string>
<string name="sftp_storage_preference_storage_location_already_configured">Platsen har redan ställts in</string>
<string name="sftp_storage_preference_click_to_select">klicka för att välja</string>
<string name="sftp_storage_preference_display_name">Namn att visa</string>
<string name="sftp_storage_preference_display_name_already_used">Namn att visa används redan</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">Namn att visa kan inte vara tomt</string>
<string name="sftp_action_mode_menu_delete">Ta bort</string>
<string name="sftp_no_sdcard_detected">Inga SD-kort detekterades</string>
<string name="sftp_no_storage_locations_configured">Inga lagringsplatser inställda</string>
<string name="sftp_saf_permission_explanation">För att komma åt filer från en annan apparat måste lagringsplatser ställas in</string>
<string name="add_host">Lägg till värddator/IP-adress</string>
<string name="add_host_hint">Värddatornamn eller IP</string>
<string name="no_players_connected">Inga spelare hittades</string>
<string name="mpris_player_on_device">%1$s på %2$s</string>
<string name="send_files">Skicka filer</string>
@@ -218,7 +238,6 @@
<string name="permission_explanation">Insticksprogrammet behöver rättigheter för att fungera</string>
<string name="optional_permission_explanation">Du måste ge extra rättigheter för att aktivera alla funktioner</string>
<string name="plugins_need_optional_permission">Vissa insticksprogram har inaktiverade funktioner på grund av att rättigheter saknas (rör för mer information):</string>
<string name="sftp_permission_explanation">För att komma åt filerna från din dator behöver applikationen rättighet att komma åt telefonens lagringsutrymme</string>
<string name="share_optional_permission_explanation">För att dela filer mellan telefonen och skrivbordet behöver du ge tillgång till telefonens lagringsutrymme</string>
<string name="telepathy_permission_explanation">För att läsa och skriva SMS från skrivbordet måste du ge rättigheter för SMS</string>
<string name="telephony_permission_explanation">För att se telefonsamtal och SMS från skrivbordet måste du ge rättigheter för telefonsamtal och SMS</string>
@@ -273,6 +292,6 @@
<string name="block_contents">Blockera underrättelsernas innehåll</string>
<string name="block_images">Blockera bilder i underrättelser</string>
<string name="notification_channel_receivenotification">Underrättelser från andra apparater</string>
<string name="take_picture">Ta bild</string>
<string name="plugin_photo_desc">Ta en bild och skicka den till en annan apparat</string>
<string name="take_picture">Starta kamera</string>
<string name="plugin_photo_desc">Starta kameraprogrammet för att förenkla att ta och överföra bilder</string>
</resources>

View File

@@ -139,11 +139,11 @@
<string name="title_activity_notification_filter">Bildirim süzgeci</string>
<string name="filter_apps_info">Bildirimler, seçili uygulamalar için eşitlenecektir.</string>
<string name="sftp_internal_storage">Harici depolama</string>
<string name="sftp_all_files">Tüm dosyalar</string>
<string name="sftp_sdcard_num">SD kart %d</string>
<string name="sftp_sdcard">SD kart</string>
<string name="sftp_readonly">(salt okunur)</string>
<string name="sftp_camera">Kamera resimleri</string>
<string name="add_host">Makine/IP ekle</string>
<string name="no_players_connected">Onatıcı bulunamadı</string>
<string name="mpris_player_on_device">%2$s üzerindeki %1$s</string>
<string name="send_files">Dosyaları gönder</string>
@@ -170,7 +170,6 @@
<string name="permission_explanation">Bu eklenti, çalışmak için izne ihtiyaç duyuyor</string>
<string name="optional_permission_explanation">Tüm işlevleri etkinleştirmek için daha fazla yetkiye ihtiyacınız var</string>
<string name="plugins_need_optional_permission">Bazı eklentilerin özellikleri, izin yetersizliğinden kapalı gelmektedir (daha fazla bilgi için dokunun):</string>
<string name="sftp_permission_explanation">Bilgisayarınızdaki dosyalara erişmek için, uygulama telefonunuzun depolama alanına erişim izni olmalıdır</string>
<string name="share_optional_permission_explanation">Telefon ve masaüstünüz arasında dosya paylaşılabilmesi için, telefonun depolama alanına erişim izni olmalıdır</string>
<string name="telepathy_permission_explanation">Masaüstünde SMS yazma ve okuma yapmak için SMS izni gereklidir</string>
<string name="telephony_permission_explanation">Masaüstünden telefon çağrılarını ve SMS görebilmek için izin gereklidir</string>

View File

@@ -38,6 +38,7 @@
<string name="open_settings">Відкрити вікно параметрів</string>
<string name="no_permissions">Вам слід надати доступ до сповіщень</string>
<string name="no_permission_mprisreceiver">Щоб мати змогу керувати вашими програвачами мультимедійних даних, вам слід надати доступ до сповіщень.</string>
<string name="no_permissions_remotekeyboard">Щоб отримувати повідомлення щодо натискання клавіш, вам слід активувати віддалену клавіатуру KDE Connect</string>
<string name="send_ping">Надіслати сигнал підтримання зв’язку</string>
<string name="open_mpris_controls">Керування відтворенням</string>
<string name="remotekeyboard_editing_only_title">Обробляти віддалені клавіші лише під час редагування</string>
@@ -194,13 +195,32 @@
<string name="title_activity_notification_filter">Фільтр сповіщень</string>
<string name="filter_apps_info">Сповіщення буде синхронізовано для позначених програм.</string>
<string name="sftp_internal_storage">Вбудоване сховище даних</string>
<string name="sftp_all_files">Усі файли</string>
<string name="sftp_sdcard_num">Картка SD %d</string>
<string name="sftp_sdcard">Картка SD</string>
<string name="sftp_readonly">(лише читання)</string>
<string name="sftp_camera">Знімки фотоапарата</string>
<string name="add_device_dialog_title">Додавання пристрою</string>
<string name="add_device_hint">Назва або IP-адреса вузла</string>
<string name="sftp_preference_detected_sdcards">Виявлені картки SD</string>
<string name="sftp_preference_edit_sdcard_title">Редагувати картку SD</string>
<string name="sftp_preference_configured_storage_locations">Налаштовані розташування сховищ</string>
<string name="sftp_preference_add_storage_location_title">Додати розташування сховища</string>
<string name="sftp_preference_edit_storage_location">Редагувати розташування сховища</string>
<string name="sftp_preference_add_camera_shortcut">Додати кнопку для теки камери</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">Додати кнопку для теки камери</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">Не додавати кнопку для теки камери</string>
<string name="sftp_storage_preference_storage_location">Розташування сховища</string>
<string name="sftp_storage_preference_storage_location_already_configured">Це розташування вже налаштовано</string>
<string name="sftp_storage_preference_click_to_select">клацання для позначення</string>
<string name="sftp_storage_preference_display_name">Назва дисплея</string>
<string name="sftp_storage_preference_display_name_already_used">Цю назву дисплея вже використано</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">Назва дисплея не може бути порожньою</string>
<string name="sftp_action_mode_menu_delete">Вилучити</string>
<string name="sftp_no_sdcard_detected">Не виявлено карток SD</string>
<string name="sftp_no_storage_locations_configured">Не налаштовано розташувань сховищ</string>
<string name="sftp_saf_permission_explanation">Щоб отримувати віддалений доступ до файлів, вам слід налаштувати розташування сховищ</string>
<string name="add_host">Додати вузол/IP</string>
<string name="add_host_hint">Назва або IP-адреса вузла</string>
<string name="no_players_connected">Не знайдено програвачів</string>
<string name="mpris_player_on_device">%1$s на %2$s</string>
<string name="send_files">Надіслати файли</string>
@@ -228,7 +248,6 @@
<string name="permission_explanation">Для роботи цього додатка потрібні додаткові права доступу</string>
<string name="optional_permission_explanation">Щоб уможливити використання усіх функцій, вам слід надати програмі додаткові права доступу</string>
<string name="plugins_need_optional_permission">Можливості деяких додатків вимкнено, оскільки програмі не вистачає прав доступу (натисніть, щоб дізнатися більше):</string>
<string name="sftp_permission_explanation">Для доступу до ваших файлі із персонального комп’ютера програмі потрібні права доступу до сховища даних вашого телефону</string>
<string name="share_optional_permission_explanation">Щоб спільного використовувати файли на вашому телефоні і робочому комп’ютері, вам слід надати програмі доступ до сховища даних вашого телефону</string>
<string name="telepathy_permission_explanation">Щоб читати і писати SMS з вашого робочого комп’ютера, вам слід надати програмі доступ до SMS</string>
<string name="telephony_permission_explanation">"Щоб переглядати дзвінки і SMS з робочого комп’ютера, вам слід надати програмі доступ до дзвінків і SMS"</string>
@@ -283,6 +302,6 @@
<string name="block_contents">Блокувати вміст сповіщень</string>
<string name="block_images">Блокувати зображення у сповіщеннях</string>
<string name="notification_channel_receivenotification">Сповіщення з інших пристроїв</string>
<string name="take_picture">Створити знімок</string>
<string name="plugin_photo_desc">Зробити знімок і надіслати його на інший пристрій</string>
<string name="take_picture">Запустити камеру</string>
<string name="plugin_photo_desc">Запустити додаток камери для спрощення знімання та передавання фотографій</string>
</resources>

View File

@@ -174,7 +174,6 @@
<string name="title_activity_notification_filter">通知过滤器</string>
<string name="filter_apps_info">所选软件的通知将会被同步。</string>
<string name="sftp_internal_storage">内部存储</string>
<string name="sftp_all_files">所有文件</string>
<string name="sftp_sdcard_num">SD卡%d</string>
<string name="sftp_sdcard">SD卡</string>
<string name="sftp_readonly">(只读)</string>
@@ -206,7 +205,6 @@
<string name="permission_explanation">这个插件需要权限才能工作</string>
<string name="optional_permission_explanation">您需要授予额外权限以启用全部功能</string>
<string name="plugins_need_optional_permission">因缺少权限,某些插件的一些功能已禁用(点击以查看更多信息):</string>
<string name="sftp_permission_explanation">此应用需要手机存储权限才能从您的 PC 访问手机内的文件</string>
<string name="share_optional_permission_explanation">您需要给予访问手机存储的权限才能在手机和桌面计算机之间分享文件</string>
<string name="telepathy_permission_explanation">从计算机桌面读取、写入短消息需要向应用程序授予 SMS 权限</string>
<string name="telephony_permission_explanation">您必须给予访问手机通话和短信的权限才能从桌面计算机查看通话记录和短信</string>

View File

@@ -38,6 +38,7 @@
<string name="open_settings">開啟設定</string>
<string name="no_permissions">您需要授予存取通知的權限</string>
<string name="no_permission_mprisreceiver">為了要能控制您的媒體播放器,您需要提供「通知」的權限</string>
<string name="no_permissions_remotekeyboard">若要接收鍵盤按鍵事件,您需要啟用 KDE 連線遠端鍵盤功能</string>
<string name="send_ping">傳送Ping回應封包</string>
<string name="open_mpris_controls">多媒體控制</string>
<string name="remotekeyboard_editing_only_title">當編輯時只處理遠端按鍵</string>
@@ -179,13 +180,32 @@
<string name="title_activity_notification_filter">通知過濾器</string>
<string name="filter_apps_info">將會以您選擇的App應用程式啟用同步通知</string>
<string name="sftp_internal_storage">內部儲存空間</string>
<string name="sftp_all_files">全部檔案</string>
<string name="sftp_sdcard_num">SD卡 %d</string>
<string name="sftp_sdcard">SD卡</string>
<string name="sftp_readonly">(唯讀)</string>
<string name="sftp_camera">相機圖片</string>
<string name="add_device_dialog_title">新增裝置</string>
<string name="add_device_hint">主機名稱 或 IP 位址</string>
<string name="sftp_preference_detected_sdcards">已偵測到 SD 卡</string>
<string name="sftp_preference_edit_sdcard_title">編輯 SD 卡</string>
<string name="sftp_preference_configured_storage_locations">已設定儲存空間位置</string>
<string name="sftp_preference_add_storage_location_title">新增儲存空間位置</string>
<string name="sftp_preference_edit_storage_location">編輯儲存空間位置</string>
<string name="sftp_preference_add_camera_shortcut">新增相機資料夾的捷徑</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">新增連結到相機資料夾的捷徑</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">請勿新增連結到相機資料夾的捷徑</string>
<string name="sftp_storage_preference_storage_location">儲存空間位置</string>
<string name="sftp_storage_preference_storage_location_already_configured">此位置已被設定</string>
<string name="sftp_storage_preference_click_to_select">按一下選擇</string>
<string name="sftp_storage_preference_display_name">顯示名稱</string>
<string name="sftp_storage_preference_display_name_already_used">此顯示名稱已被使用</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">顯示名稱不得空白</string>
<string name="sftp_action_mode_menu_delete">刪除</string>
<string name="sftp_no_sdcard_detected">未偵測到 SD 卡</string>
<string name="sftp_no_storage_locations_configured">未設定儲存空間位置</string>
<string name="sftp_saf_permission_explanation">若要遠端存取檔案,您需先設定儲存空間位置</string>
<string name="add_host">增加 host/IP</string>
<string name="add_host_hint">主機名稱或 IP</string>
<string name="no_players_connected">沒有發現播放器</string>
<string name="mpris_player_on_device">%1$s on %2$s</string>
<string name="send_files">傳送檔案</string>
@@ -213,7 +233,6 @@
<string name="permission_explanation">這附加元件需要權限以運作</string>
<string name="optional_permission_explanation">你需要授予延伸的權限以啟用所有的功能</string>
<string name="plugins_need_optional_permission">部份的附加元件因為缺乏權限,而導致功能被停用。(點擊以了解更多資訊):</string>
<string name="sftp_permission_explanation">為了要從您的個人電腦存取檔案,這個應用程式需要權限以存取您的手機儲存空間。</string>
<string name="share_optional_permission_explanation">為了要在您的手機與電腦之間分享檔案,你需要同意存取手機的儲存空間。</string>
<string name="telepathy_permission_explanation">為了要在您的個人電腦上讀取與撰寫簡訊,你需要提供簡訊的權限。</string>
<string name="telephony_permission_explanation">為了要在您的電腦上檢視手機通話與簡訊,你需要提供手機通話與簡訊的權限。</string>
@@ -268,4 +287,6 @@
<string name="block_contents">擋住通知內容</string>
<string name="block_images">擋住通知中的圖片</string>
<string name="notification_channel_receivenotification">其他裝置上的通知</string>
<string name="take_picture">啟動相機</string>
<string name="plugin_photo_desc">開啟相機應用程式以輕鬆拍攝並傳輸相片</string>
</resources>

View File

@@ -39,6 +39,7 @@
<string name="open_settings">Open settings</string>
<string name="no_permissions">You need to grant permission to access notifications</string>
<string name="no_permission_mprisreceiver">To be able to control your media players you need to grant access to the notifications</string>
<string name="no_permissions_remotekeyboard">To receive keypresses you need to activate the KDE Connect Remote Keyboard</string>
<string name="send_ping">Send ping</string>
<string name="open_mpris_controls">Multimedia control</string>
<string name="remotekeyboard_editing_only" translatable="false">remotekeyboard_editing_only</string>
@@ -225,13 +226,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 +287,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>

View File

@@ -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>

View File

@@ -0,0 +1,22 @@
<?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>

View File

@@ -419,7 +419,7 @@ public class BackgroundService extends Service {
}).start();
}
public static <T extends Plugin> void runWithPlugin(final Context c, final String deviceId, final Class<T> pluginClass, final PluginCallback<T> cb) {
public static <T extends Plugin> void RunWithPlugin(final Context c, final String deviceId, final Class<T> pluginClass, final PluginCallback<T> cb) {
RunCommand(c, service -> {
Device device = service.getDevice(deviceId);

View 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;
}
}

View File

@@ -222,7 +222,7 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
mCurrentX = event.getX();
mCurrentY = event.getY();
BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> {
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> {
float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity;
float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity;
@@ -293,7 +293,7 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
@Override
public void onLongPress(MotionEvent e) {
getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleHold);
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleHold);
}
@Override
@@ -303,13 +303,13 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleClick);
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleClick);
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendDoubleClick);
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendDoubleClick);
return true;
}
@@ -348,15 +348,15 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
private void sendMiddleClick() {
BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendMiddleClick);
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendMiddleClick);
}
private void sendRightClick() {
BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendRightClick);
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendRightClick);
}
private void sendScroll(final float y) {
BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendScroll(0, y));
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendScroll(0, y));
}
//TODO: Does not work on KitKat with or without requestFocus()

View File

@@ -326,7 +326,8 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
.setSmallIcon(R.drawable.ic_play_white)
.setShowWhen(false)
.setColor(service.getResources().getColor(R.color.primary))
.setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC);
.setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC)
.setSubText(service.getDevice(notificationDevice).getName());
if (!notificationPlayer.getTitle().isEmpty()) {
notification.setContentTitle(notificationPlayer.getTitle());

View File

@@ -46,22 +46,11 @@ class MprisReceiverCallback extends MediaController.Callback {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onPlaybackStateChanged(@NonNull PlaybackState state) {
switch (state.getState()) {
case PlaybackState.STATE_PLAYING:
player.setPlaying(true);
plugin.sendPlaying(player);
break;
case PlaybackState.STATE_PAUSED:
player.setPaused(true);
plugin.sendPlaying(player);
break;
}
plugin.sendMetadata(player);
}
@Override
public void onMetadataChanged(@Nullable MediaMetadata metadata) {
if (metadata == null)
return;
plugin.sendMetadata(player);
}

View File

@@ -34,36 +34,59 @@ class MprisReceiverPlayer {
private final String name;
private boolean isPlaying;
MprisReceiverPlayer(MediaController controller, String name) {
this.controller = controller;
this.name = name;
if (controller.getPlaybackState() != null) {
isPlaying = controller.getPlaybackState().getState() == PlaybackState.STATE_PLAYING;
}
}
boolean isPlaying() {
return isPlaying;
PlaybackState state = controller.getPlaybackState();
if (state == null) return false;
return state.getState() == PlaybackState.STATE_PLAYING;
}
void setPlaying(boolean playing) {
isPlaying = playing;
boolean canPlay() {
PlaybackState state = controller.getPlaybackState();
if (state == null) return false;
if (state.getState() == PlaybackState.STATE_PLAYING) return true;
return (state.getActions() & (PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE)) != 0;
}
boolean isPaused() {
return !isPlaying;
boolean canPause() {
PlaybackState state = controller.getPlaybackState();
if (state == null) return false;
if (state.getState() == PlaybackState.STATE_PAUSED) return true;
return (state.getActions() & (PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY_PAUSE)) != 0;
}
void setPaused(boolean paused) {
isPlaying = !paused;
boolean canGoPrevious() {
PlaybackState state = controller.getPlaybackState();
if (state == null) return false;
return (state.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0;
}
boolean canGoNext() {
PlaybackState state = controller.getPlaybackState();
if (state == null) return false;
return (state.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0;
}
boolean canSeek() {
PlaybackState state = controller.getPlaybackState();
if (state == null) return false;
return (state.getActions() & PlaybackState.ACTION_SEEK_TO) != 0;
}
void playPause() {
if (isPlaying) {
if (isPlaying()) {
controller.getTransportControls().pause();
} else {
controller.getTransportControls().play();
@@ -75,24 +98,31 @@ class MprisReceiverPlayer {
}
String getAlbum() {
if (controller.getMetadata() == null)
return "";
String album = controller.getMetadata().getString(MediaMetadata.METADATA_KEY_ALBUM);
MediaMetadata metadata = controller.getMetadata();
if (metadata == null) return "";
String album = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM);
return album != null ? album : "";
}
String getArtist() {
if (controller.getMetadata() == null)
return "";
MediaMetadata metadata = controller.getMetadata();
if (metadata == null) return "";
String artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
if (artist == null || artist.isEmpty()) artist = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST);
if (artist == null || artist.isEmpty()) artist = metadata.getString(MediaMetadata.METADATA_KEY_AUTHOR);
if (artist == null || artist.isEmpty()) artist = metadata.getString(MediaMetadata.METADATA_KEY_WRITER);
String artist = controller.getMetadata().getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST);
return artist != null ? artist : "";
}
String getTitle() {
if (controller.getMetadata() == null)
return "";
String title = controller.getMetadata().getString(MediaMetadata.METADATA_KEY_TITLE);
MediaMetadata metadata = controller.getMetadata();
if (metadata == null) return "";
String title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
if (title == null || title.isEmpty()) title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE);
return title != null ? title : "";
}
@@ -104,6 +134,18 @@ class MprisReceiverPlayer {
controller.getTransportControls().skipToNext();
}
void play() {
controller.getTransportControls().play();
}
void pause() {
controller.getTransportControls().pause();
}
void stop() {
controller.getTransportControls().stop();
}
int getVolume() {
if (controller.getPlaybackInfo() == null)
return 0;
@@ -115,4 +157,15 @@ class MprisReceiverPlayer {
return 0;
return controller.getPlaybackState().getPosition();
}
void setPosition(long position) {
controller.getTransportControls().seekTo(position);
}
long getLength() {
MediaMetadata metadata = controller.getMetadata();
if (metadata == null) return 0;
return metadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
}
}

View File

@@ -45,6 +45,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
//FIXME: Breaks on Android 4 because it extends OnActiveSessionsChangedListener
//@PluginFactory.LoadablePlugin
public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.OnActiveSessionsChangedListener {
@@ -78,6 +79,15 @@ public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.O
return true;
}
@Override
public void onDestroy() {
super.onDestroy();
MediaSessionManager manager = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
if (manager != null) {
manager.removeOnActiveSessionsChangedListener(MprisReceiverPlugin.this);
}
}
private void createPlayers(List<MediaController> sessions) {
for (MediaController controller : sessions) {
createPlayer(controller);
@@ -116,10 +126,21 @@ public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.O
return true;
}
if (np.has("SetPosition")) {
long position = np.getLong("SetPosition", 0);
player.setPosition(position);
}
if (np.has("action")) {
String action = np.getString("action");
switch (action) {
case "Play":
player.play();
break;
case "Pause":
player.pause();
break;
case "PlayPause":
player.playPause();
break;
@@ -128,6 +149,10 @@ public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.O
break;
case "Previous":
player.previous();
break;
case "Stop":
player.stop();
break;
}
}
@@ -159,6 +184,9 @@ public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.O
}
private void createPlayer(MediaController controller) {
// Skip the media session we created ourselves as KDE Connect
if (controller.getPackageName().equals(context.getPackageName())) return;
MprisReceiverPlayer player = new MprisReceiverPlayer(controller, AppsHelper.appNameLookup(context, controller.getPackageName()));
controller.registerCallback(new MprisReceiverCallback(this, player), new Handler(Looper.getMainLooper()));
players.put(player.getName(), player);
@@ -170,20 +198,12 @@ public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.O
device.sendPacket(np);
}
void sendPlaying(MprisReceiverPlayer player) {
NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS);
np.set("player", player.getName());
np.set("isPlaying", player.isPlaying());
device.sendPacket(np);
}
@Override
public int getMinSdk() {
return Build.VERSION_CODES.LOLLIPOP_MR1;
}
public void sendMetadata(MprisReceiverPlayer player) {
void sendMetadata(MprisReceiverPlayer player) {
NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS);
np.set("player", player.getName());
if (player.getArtist().isEmpty()) {
@@ -196,12 +216,12 @@ public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.O
np.set("album", player.getAlbum());
np.set("isPlaying", player.isPlaying());
np.set("pos", player.getPosition());
device.sendPacket(np);
}
public void sendVolume(MprisReceiverPlayer player) {
NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS);
np.set("player", player.getName());
np.set("length", player.getLength());
np.set("canPlay", player.canPlay());
np.set("canPause", player.canPause());
np.set("canGoPrevious", player.canGoPrevious());
np.set("canGoNext", player.canGoNext());
np.set("canSeek", player.canSeek());
np.set("volume", player.getVolume());
device.sendPacket(np);
}

View File

@@ -25,7 +25,7 @@ public class PhotoActivity extends AppCompatActivity {
protected void onStart() {
super.onStart();
BackgroundService.runWithPlugin(this, getIntent().getStringExtra("deviceId"), PhotoPlugin.class, plugin -> {
BackgroundService.RunWithPlugin(this, getIntent().getStringExtra("deviceId"), PhotoPlugin.class, plugin -> {
this.plugin = plugin;
});

View File

@@ -27,6 +27,7 @@ import android.util.Log;
import org.atteo.classindex.ClassIndex;
import org.atteo.classindex.IndexAnnotated;
import org.kde.kdeconnect.Device;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;

View File

@@ -52,7 +52,7 @@ public class PresenterActivity extends AppCompatActivity {
final String deviceId = getIntent().getStringExtra("deviceId");
BackgroundService.runWithPlugin(this, deviceId, PresenterPlugin.class, plugin -> runOnUiThread(() -> {
BackgroundService.RunWithPlugin(this, deviceId, PresenterPlugin.class, plugin -> runOnUiThread(() -> {
this.plugin = plugin;
findViewById(R.id.next_button).setOnClickListener(v -> plugin.sendNext());
findViewById(R.id.previous_button).setOnClickListener(v -> plugin.sendPrevious());

View File

@@ -23,6 +23,7 @@ package org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.KeyEvent;
@@ -34,6 +35,8 @@ import android.view.inputmethod.InputConnection;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.AlertDialogFragment;
import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
@@ -393,4 +396,22 @@ public class RemoteKeyboardPlugin extends Plugin {
String getDeviceId() {
return device.getDeviceId();
}
@Override
public boolean checkRequiredPermissions() {
return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_INPUT_METHODS).contains("org.kde.kdeconnect_tp");
}
@Override
public AlertDialogFragment getPermissionExplanationDialog(int requestCode) {
return new StartActivityAlertDialogFragment.Builder()
.setTitle(R.string.pref_plugin_remotekeyboard)
.setMessage(R.string.no_permissions_remotekeyboard)
.setPositiveButton(R.string.open_settings)
.setNegativeButton(R.string.cancel)
.setIntentAction(Settings.ACTION_INPUT_METHOD_SETTINGS)
.setStartForResult(true)
.setRequestCode(requestCode)
.create();
}
}

View File

@@ -25,7 +25,6 @@ import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -40,7 +39,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R;
@@ -59,15 +57,8 @@ public class RunCommandActivity extends AppCompatActivity {
private ArrayList<ListAdapter.Item> commandItems;
private void updateView() {
BackgroundService.RunCommand(this, service -> {
final Device device = service.getDevice(deviceId);
final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
if (plugin == null) {
Log.e("RunCommandActivity", "device has no runcommand plugin!");
return;
}
BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> {
runOnUiThread(() -> {
ListView view = findViewById(R.id.runcommandslist);
@@ -120,27 +111,25 @@ public class RunCommandActivity extends AppCompatActivity {
boolean canAddCommands = BackgroundService.getInstance().getDevice(deviceId).getPlugin(RunCommandPlugin.class).canAddCommand();
FloatingActionButton addCommandButton = findViewById(R.id.add_command_button);
addCommandButton.setVisibility(canAddCommands ? View.VISIBLE : View.GONE);
if (canAddCommands) {
addCommandButton.show();
} else {
addCommandButton.hide();
}
addCommandButton.setOnClickListener(view -> BackgroundService.RunCommand(RunCommandActivity.this, service -> {
addCommandButton.setOnClickListener(v -> {
final Device device = service.getDevice(deviceId);
final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
if (plugin == null) {
Log.e("RunCommandActivity", "device has no runcommand plugin!");
return;
}
BackgroundService.RunWithPlugin(RunCommandActivity.this, deviceId, RunCommandPlugin.class, plugin -> {
plugin.sendSetupPacket();
AlertDialog dialog = new AlertDialog.Builder(RunCommandActivity.this)
.setTitle(R.string.add_command)
.setMessage(R.string.add_command_description)
.setPositiveButton(R.string.ok, null)
.create();
dialog.show();
});
plugin.sendSetupPacket();
AlertDialog dialog = new AlertDialog.Builder(RunCommandActivity.this)
.setTitle(R.string.add_command)
.setMessage(R.string.add_command_description)
.setPositiveButton(R.string.ok, null)
.create();
dialog.show();
}));
});
updateView();
}
@@ -171,14 +160,7 @@ public class RunCommandActivity extends AppCompatActivity {
protected void onResume() {
super.onResume();
BackgroundService.RunCommand(this, service -> {
final Device device = service.getDevice(deviceId);
final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
if (plugin == null) {
Log.e("RunCommandActivity", "device has no runcommand plugin!");
return;
}
BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> {
plugin.addCommandsUpdatedCallback(commandsChangedCallback);
});
}
@@ -187,14 +169,7 @@ public class RunCommandActivity extends AppCompatActivity {
protected void onPause() {
super.onPause();
BackgroundService.RunCommand(this, service -> {
final Device device = service.getDevice(deviceId);
final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
if (plugin == null) {
Log.e("RunCommandActivity", "device has no runcommand plugin!");
return;
}
BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> {
plugin.removeCommandsUpdatedCallback(commandsChangedCallback);
});
}

View File

@@ -0,0 +1,70 @@
/*
* 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);
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,499 @@
/*
* 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.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.text.TextUtils;
import android.util.Log;
import org.apache.sshd.common.file.SshFile;
import org.kde.kdeconnect.Helpers.FilesHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import androidx.annotation.Nullable;
@TargetApi(21)
public class AndroidSafSshFile implements SshFile {
private static final String TAG = AndroidSafSshFile.class.getSimpleName();
private final String virtualFileName;
private DocumentInfo documentInfo;
private Uri parentUri;
private final AndroidSafFileSystemView fileSystemView;
AndroidSafSshFile(final AndroidSafFileSystemView fileSystemView, Uri parentUri, Uri uri, String virtualFileName) {
this.fileSystemView = fileSystemView;
this.parentUri = parentUri;
this.documentInfo = new DocumentInfo(fileSystemView.context, uri);
this.virtualFileName = virtualFileName;
}
@Override
public String getAbsolutePath() {
return virtualFileName;
}
@Override
public String getName() {
/* From NativeSshFile, looks a lot like new File(virtualFileName).getName() to me */
// strip the last '/'
String shortName = virtualFileName;
int filelen = virtualFileName.length();
if (shortName.charAt(filelen - 1) == File.separatorChar) {
shortName = shortName.substring(0, filelen - 1);
}
// return from the last '/'
int slashIndex = shortName.lastIndexOf(File.separatorChar);
if (slashIndex != -1) {
shortName = shortName.substring(slashIndex + 1);
}
return shortName;
}
@Override
public String getOwner() {
return fileSystemView.userName;
}
@Override
public boolean isDirectory() {
return documentInfo.isDirectory;
}
@Override
public boolean isFile() {
return documentInfo.isFile;
}
@Override
public boolean doesExist() {
return documentInfo.exists;
}
@Override
public long getSize() {
return documentInfo.length;
}
@Override
public long getLastModified() {
return documentInfo.lastModified;
}
@Override
public boolean setLastModified(long time) {
//TODO
/* Throws UnsupportedOperationException on API 26
try {
ContentValues updateValues = new ContentValues();
updateValues.put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, time);
result = fileSystemView.context.getContentResolver().update(documentInfo.uri, updateValues, null, null) != 0;
documentInfo.lastModified = time;
} catch (NullPointerException ignored) {}
*/
return true;
}
@Override
public boolean isReadable() {
return documentInfo.canRead;
}
@Override
public boolean isWritable() {
return documentInfo.canWrite;
}
@Override
public boolean isExecutable() {
return documentInfo.isDirectory;
}
@Override
public boolean isRemovable() {
Log.d(TAG, "isRemovable() - is this ever called?");
return false;
}
public SshFile getParentFile() {
Log.d(TAG,"getParentFile() - is this ever called");
return null;
}
@Override
public boolean delete() {
boolean ret;
try {
ret = DocumentsContract.deleteDocument(fileSystemView.context.getContentResolver(), documentInfo.uri);
} catch (FileNotFoundException e) {
ret = false;
}
return ret;
}
@Override
public boolean create() {
return create(parentUri, FilesHelper.getMimeTypeFromFile(virtualFileName), getName());
}
private boolean create(Uri parentUri, String mimeType, String name) {
Uri uri = null;
try {
uri = DocumentsContract.createDocument(fileSystemView.context.getContentResolver(), parentUri, mimeType, name);
if (uri != null) {
documentInfo = new DocumentInfo(fileSystemView.context, uri);
}
} catch (FileNotFoundException ignored) {}
return uri != null;
}
@Override
public void truncate() throws IOException {
if (documentInfo.length > 0) {
delete();
create();
}
}
@Override
public boolean move(final SshFile dest) {
boolean success = false;
Uri destParentUri = ((AndroidSafSshFile)dest).parentUri;
if (destParentUri.equals(parentUri)) {
//Rename
try {
Uri newUri = DocumentsContract.renameDocument(fileSystemView.context.getContentResolver(), documentInfo.uri, dest.getName());
if (newUri != null) {
success = true;
documentInfo.uri = newUri;
}
} catch (FileNotFoundException ignored) {}
} else {
// Move:
String sourceTreeDocumentId = DocumentsContract.getTreeDocumentId(parentUri);
String destTreeDocumentId = DocumentsContract.getTreeDocumentId(((AndroidSafSshFile) dest).parentUri);
if (sourceTreeDocumentId.equals(destTreeDocumentId) && Build.VERSION.SDK_INT >= 24) {
try {
Uri newUri = DocumentsContract.moveDocument(fileSystemView.context.getContentResolver(), documentInfo.uri, parentUri, destParentUri);
if (newUri != null) {
success = true;
parentUri = destParentUri;
documentInfo.uri = newUri;
}
} catch (Exception ignored) {
Log.e(TAG,"DocumentsContract.moveDocument() threw an exception: " + ignored.getMessage());
}
} else {
try {
if (dest.create()) {
try (InputStream in = createInputStream(0); OutputStream out = dest.createOutputStream(0)) {
byte[] buffer = new byte[10 * 1024];
int read;
while ((read = in.read(buffer)) > 0) {
out.write(buffer, 0, read);
}
out.flush();
delete();
success = true;
} catch (IOException e) {
if (dest.doesExist()) {
dest.delete();
}
}
}
} catch (IOException ignored) {}
}
}
return success;
}
@Override
public boolean mkdir() {
return create(parentUri, DocumentsContract.Document.MIME_TYPE_DIR, getName());
}
@Override
public List<SshFile> listSshFiles() {
if (!documentInfo.isDirectory) {
return null;
}
final ContentResolver resolver = fileSystemView.context.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(documentInfo.uri, DocumentsContract.getDocumentId(documentInfo.uri));
final ArrayList<AndroidSafSshFile> results = new ArrayList<>();
Cursor c = resolver.query(childrenUri, new String[]
{ DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME }, null, null, null);
while (c != null && c.moveToNext()) {
final String documentId = c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID));
final String displayName = c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(documentInfo.uri, documentId);
results.add(new AndroidSafSshFile(fileSystemView, parentUri, documentUri, virtualFileName + File.separatorChar + displayName));
}
if (c != null) {
c.close();
}
return Collections.unmodifiableList(results);
}
@Override
public OutputStream createOutputStream(final long offset) throws IOException {
return fileSystemView.context.getContentResolver().openOutputStream(documentInfo.uri);
}
@Override
public InputStream createInputStream(final long offset) throws IOException {
return fileSystemView.context.getContentResolver().openInputStream(documentInfo.uri);
}
@Override
public void handleClose() {
// Nop
}
@Override
public Map<Attribute, Object> getAttributes(boolean followLinks) throws IOException {
Map<SshFile.Attribute, Object> attributes = new HashMap<>();
for (SshFile.Attribute attr : SshFile.Attribute.values()) {
switch (attr) {
case Uid:
case Gid:
case NLink:
continue;
}
attributes.put(attr, getAttribute(attr, followLinks));
}
return attributes;
}
@Override
public Object getAttribute(Attribute attribute, boolean followLinks) throws IOException {
Object ret;
switch (attribute) {
case Size:
ret = documentInfo.length;
break;
case Uid:
ret = 1;
break;
case Owner:
ret = getOwner();
break;
case Gid:
ret = 1;
break;
case Group:
ret = getOwner();
break;
case IsDirectory:
ret = documentInfo.isDirectory;
break;
case IsRegularFile:
ret = documentInfo.isFile;
break;
case IsSymbolicLink:
ret = false;
break;
case Permissions:
Set<Permission> tmp = new HashSet<>();
if (documentInfo.canRead) {
tmp.add(SshFile.Permission.UserRead);
tmp.add(SshFile.Permission.GroupRead);
tmp.add(SshFile.Permission.OthersRead);
}
if (documentInfo.canWrite) {
tmp.add(SshFile.Permission.UserWrite);
tmp.add(SshFile.Permission.GroupWrite);
tmp.add(SshFile.Permission.OthersWrite);
}
if (isExecutable()) {
tmp.add(SshFile.Permission.UserExecute);
tmp.add(SshFile.Permission.GroupExecute);
tmp.add(SshFile.Permission.OthersExecute);
}
ret = tmp.isEmpty()
? EnumSet.noneOf(SshFile.Permission.class)
: EnumSet.copyOf(tmp);
break;
case CreationTime:
ret = documentInfo.lastModified;
break;
case LastModifiedTime:
ret = documentInfo.lastModified;
break;
case LastAccessTime:
ret = documentInfo.lastModified;
break;
case NLink:
ret = 0;
break;
default:
ret = null;
break;
}
return ret;
}
@Override
public void setAttributes(Map<Attribute, Object> attributes) {
//TODO: Using Java 7 NIO it should be possible to implement setting a number of attributes but does SaF allow that?
Log.d(TAG, "setAttributes()");
}
@Override
public void setAttribute(Attribute attribute, Object value) throws IOException {
Log.d(TAG, "setAttribute()");
}
@Override
public String readSymbolicLink() throws IOException {
throw new IOException("Not Implemented");
}
@Override
public void createSymbolicLink(SshFile destination) throws IOException {
throw new IOException("Not Implemented");
}
/**
* Retrieve all file info using 1 query to speed things up
* The only fields guaranteed to be initialized are uri and exists
*/
private static class DocumentInfo {
private Uri uri;
private boolean exists;
@Nullable
private String documentId;
private boolean canRead;
private boolean canWrite;
@Nullable
private String mimeType;
private boolean isDirectory;
private boolean isFile;
private long lastModified;
private long length;
@Nullable
private String displayName;
private static final String[] columns;
static {
columns = new String[]{
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
//DocumentsContract.Document.COLUMN_ICON,
DocumentsContract.Document.COLUMN_FLAGS,
DocumentsContract.Document.COLUMN_SIZE
};
}
/*
Based on https://github.com/rcketscientist/DocumentActivity
Extracted from android.support.v4.provider.DocumentsContractAPI19 and android.support.v4.provider.DocumentsContractAPI21
*/
private DocumentInfo(Context c, Uri uri)
{
this.uri = uri;
try (Cursor cursor = c.getContentResolver().query(uri, columns, null, null, null)) {
exists = cursor != null && cursor.getCount() > 0;
if (!exists)
return;
cursor.moveToFirst();
documentId = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID));
final boolean readPerm = c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
== PackageManager.PERMISSION_GRANTED;
final boolean writePerm = c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
== PackageManager.PERMISSION_GRANTED;
final int flags = cursor.getInt(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS));
final boolean supportsDelete = (flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0;
final boolean supportsCreate = (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0;
final boolean supportsWrite = (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0;
mimeType = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE));
final boolean hasMime = !TextUtils.isEmpty(mimeType);
isDirectory = DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType);
isFile = !isDirectory && hasMime;
canRead = readPerm && hasMime;
canWrite = writePerm && (supportsDelete || (isDirectory && supportsCreate) || (hasMime && supportsWrite));
displayName = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
lastModified = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED));
length = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE));
} catch (IllegalArgumentException e) {
//File does not exist, it's probably going to be created
exists = false;
canWrite = true;
}
}
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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;
class AndroidSshFile extends NativeSshFile {
private final static String TAG = AndroidSshFile.class.getSimpleName();
final private Context context;
final private File file;
AndroidSshFile(final AndroidFileSystemView view, String name, final File file, final String userName, Context context) {
super(view, name, file, userName);
this.context = context;
this.file = file;
}
@Override
public OutputStream createOutputStream(long offset) throws IOException {
if (!isWritable()) {
throw new IOException("No write permission : " + file.getName());
}
final RandomAccessFile raf = new RandomAccessFile(file, "rw");
try {
if (offset < raf.length()) {
throw new IOException("Your SSHFS is bugged"); //SSHFS 3.0 and 3.2 cause data corruption, abort the transfer if this happens
}
raf.setLength(offset);
raf.seek(offset);
return new FileOutputStream(raf.getFD()) {
public void close() throws IOException {
super.close();
raf.close();
}
};
} catch (IOException e) {
raf.close();
throw e;
}
}
@Override
public boolean delete() {
boolean ret = super.delete();
if (ret) {
MediaStoreHelper.indexFile(context, Uri.fromFile(file));
}
return ret;
}
@Override
public boolean create() throws IOException {
boolean ret = super.create();
if (ret) {
MediaStoreHelper.indexFile(context, Uri.fromFile(file));
}
return ret;
}
// Based on https://github.com/wolpi/prim-ftpd/blob/master/primitiveFTPd/src/org/primftpd/filesystem/FsFile.java
@Override
public boolean doesExist() {
boolean exists = file.exists();
if (!exists) {
// file.exists() returns false when we don't have read permission
// try to figure out if it really does not exist
File parentFile = file.getParentFile();
File[] children = parentFile.listFiles();
if (children != null) {
for (File child : children) {
if (file.equals(child)) {
exists = true;
break;
}
}
}
}
return exists;
}
}

View File

@@ -0,0 +1,178 @@
/*
* 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 org.apache.sshd.common.file.SshFile;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
//TODO: ls .. and ls / only show .. and / respectively I would expect a listing
//TODO: cd .. to / does not work and prints "Can't change directory: Can't check target"
class RootFile implements SshFile {
private final boolean exists;
private final String userName;
private final List<SshFile> files;
RootFile(List<SshFile> files, String userName, boolean exits) {
this.files = files;
this.userName = userName;
this.exists = exits;
}
public String getAbsolutePath() {
return "/";
}
public String getName() {
return "/";
}
public Map<Attribute, Object> getAttributes(boolean followLinks) throws IOException {
Map<Attribute, Object> attrs = new HashMap<>();
attrs.put(Attribute.Size, 0);
attrs.put(Attribute.Owner, userName);
attrs.put(Attribute.Group, userName);
EnumSet<Permission> p = EnumSet.noneOf(Permission.class);
p.add(Permission.UserExecute);
p.add(Permission.GroupExecute);
p.add(Permission.OthersExecute);
attrs.put(Attribute.Permissions, p);
long now = Calendar.getInstance().getTimeInMillis();
attrs.put(Attribute.LastAccessTime, now);
attrs.put(Attribute.LastModifiedTime, now);
attrs.put(Attribute.IsSymbolicLink, false);
attrs.put(Attribute.IsDirectory, true);
attrs.put(Attribute.IsRegularFile, false);
return attrs;
}
public void setAttributes(Map<Attribute, Object> attributes) {
}
public Object getAttribute(Attribute attribute, boolean followLinks) {
return null;
}
public void setAttribute(Attribute attribute, Object value) {
}
public String readSymbolicLink() {
return null;
}
public void createSymbolicLink(SshFile destination) {
}
public String getOwner() {
return null;
}
public boolean isDirectory() {
return true;
}
public boolean isFile() {
return false;
}
public boolean doesExist() {
return exists;
}
public boolean isReadable() {
return true;
}
public boolean isWritable() {
return false;
}
public boolean isExecutable() {
return true;
}
public boolean isRemovable() {
return false;
}
public SshFile getParentFile() {
return this;
}
public long getLastModified() {
return 0;
}
public boolean setLastModified(long time) {
return false;
}
public long getSize() {
return 0;
}
public boolean mkdir() {
return false;
}
public boolean delete() {
return false;
}
public boolean create() {
return false;
}
public void truncate() {
}
public boolean move(SshFile destination) {
return false;
}
public List<SshFile> listSshFiles() {
return Collections.unmodifiableList(files);
}
public OutputStream createOutputStream(long offset) {
return null;
}
public InputStream createInputStream(long offset) {
return null;
}
public void handleClose() {
}
}

View File

@@ -15,33 +15,47 @@
* 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/>.
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Plugins.SftpPlugin;
import android.Manifest;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import org.kde.kdeconnect.Helpers.StorageHelper;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.NetworkPacket;
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.PluginSettingsFragment;
import org.kde.kdeconnect_tp.R;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
@PluginFactory.LoadablePlugin
public class SftpPlugin extends Plugin {
public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPreferenceChangeListener {
private final static String PACKET_TYPE_SFTP = "kdeconnect.sftp";
private final static String PACKET_TYPE_SFTP_REQUEST = "kdeconnect.sftp.request";
private static final SimpleSftpServer server = new SimpleSftpServer();
private String KeyStorageInfoList;
private String KeyAddCameraShortcut;
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_sftp);
@@ -54,9 +68,16 @@ public class SftpPlugin extends Plugin {
@Override
public boolean onCreate() {
permissionExplanation = R.string.sftp_permission_explanation;
KeyStorageInfoList = context.getString(R.string.sftp_preference_key_storage_info_list);
KeyAddCameraShortcut = context.getString(R.string.sftp_preference_key_add_camera_shortcut);
try {
server.init(context, device);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
return SftpSettingsFragment.getStorageInfoList(context).size() != 0;
}
return true;
} catch (Exception e) {
e.printStackTrace();
@@ -64,69 +85,77 @@ public class SftpPlugin extends Plugin {
}
}
@Override
public boolean checkOptionalPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return SftpSettingsFragment.getStorageInfoList(context).size() != 0;
}
return true;
}
@Override
public AlertDialogFragment getOptionalPermissionExplanationDialog(int requestCode) {
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
public void onDestroy() {
server.stop();
PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public boolean onPacketReceived(NetworkPacket np) {
if (np.getBoolean("startBrowsing")) {
if (server.start()) {
ArrayList<String> paths = new ArrayList<>();
ArrayList<String> pathNames = new ArrayList<>();
List<StorageInfo> storageInfoList = SftpSettingsFragment.getStorageInfoList(context);
Collections.sort(storageInfoList, new StorageInfo.UriNameComparator());
if (storageInfoList.size() > 0) {
getPathsAndNamesForStorageInfoList(paths, pathNames, storageInfoList);
} else {
NetworkPacket np2 = new NetworkPacket(PACKET_TYPE_SFTP);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
np2.set("errorMessage", context.getString(R.string.sftp_no_storage_locations_configured));
} else {
np2.set("errorMessage", context.getString(R.string.sftp_no_sdcard_detected));
}
device.sendPacket(np2);
return true;
}
removeChildren(storageInfoList);
if (server.start(storageInfoList)) {
PreferenceManager.getDefaultSharedPreferences(context).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("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", Environment.getExternalStorageDirectory().getAbsolutePath());
List<StorageHelper.StorageInfo> storageList = StorageHelper.getStorageList();
ArrayList<String> paths = new ArrayList<>();
ArrayList<String> pathNames = new ArrayList<>();
for (StorageHelper.StorageInfo storage : storageList) {
paths.add(storage.path);
StringBuilder res = new StringBuilder();
if (storageList.size() > 1) {
if (!storage.removable) {
res.append(context.getString(R.string.sftp_internal_storage));
} else if (storage.number > 1) {
res.append(context.getString(R.string.sftp_sdcard_num, storage.number));
} else {
res.append(context.getString(R.string.sftp_sdcard));
}
} else {
res.append(context.getString(R.string.sftp_all_files));
}
String pathName = res.toString();
if (storage.readonly) {
res.append(" ");
res.append(context.getString(R.string.sftp_readonly));
}
pathNames.add(res.toString());
//Shortcut for users that only want to browse camera pictures
String dcim = storage.path + "/DCIM/Camera";
if (new File(dcim).exists()) {
paths.add(dcim);
if (storageList.size() > 1) {
pathNames.add(context.getString(R.string.sftp_camera) + "(" + pathName + ")");
} else {
pathNames.add(context.getString(R.string.sftp_camera));
}
}
}
np2.set("path", "/");
if (paths.size() > 0) {
np2.set("multiPaths", paths);
np2.set("pathNames", pathNames);
}
device.sendPacket(np2);
@@ -137,12 +166,66 @@ public class SftpPlugin extends Plugin {
return false;
}
@Override
public String[] getRequiredPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE};
} else {
return new String[0];
private void getPathsAndNamesForStorageInfoList(List<String> paths, List<String> pathNames, List<StorageInfo> storageInfoList) {
StorageInfo prevInfo = null;
StringBuilder pathBuilder = new StringBuilder();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean addCameraShortcuts = false;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
addCameraShortcuts = prefs.getBoolean(context.getString(R.string.sftp_preference_key_add_camera_shortcut), true);
}
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);
if (addCameraShortcuts) {
if (new File(curInfo.uri.getPath(), "/DCIM/Camera").exists()) {
paths.add(pathBuilder.toString() + "/DCIM/Camera");
if (storageInfoList.size() > 1) {
pathNames.add(context.getString(R.string.sftp_camera) + "(" + curInfo.displayName + ")");
} else {
pathNames.add(context.getString(R.string.sftp_camera));
}
}
}
}
}
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;
}
}
}
}
@@ -156,4 +239,102 @@ public class SftpPlugin extends Plugin {
return new String[]{PACKET_TYPE_SFTP};
}
@Override
public boolean hasSettings() {
return true;
}
@Override
public PluginSettingsFragment getSettingsFragment(Activity activity) {
return SftpSettingsFragment.newInstance(getPluginKey());
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(KeyStorageInfoList) || key.equals(KeyAddCameraShortcut)) {
//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;
}
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;
}
static class DisplayNameComparator implements java.util.Comparator<StorageInfo> {
@Override
public int compare(StorageInfo si1, StorageInfo si2) {
return si1.displayName.compareToIgnoreCase(si2.displayName);
}
}
static class UriNameComparator implements java.util.Comparator<StorageInfo> {
@Override
public int compare(StorageInfo si1, StorageInfo si2) {
return si1.uri.compareTo(si2.uri);
}
}
}
}

View File

@@ -0,0 +1,499 @@
/*
* 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.content.Intent;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.util.SparseBooleanArray;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.StorageHelper;
import org.kde.kdeconnect.UserInterface.DeviceSettingsActivity;
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
import org.kde.kdeconnect_tp.R;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import androidx.annotation.NonNull;
import androidx.appcompat.view.ActionMode;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.recyclerview.widget.RecyclerView;
//TODO: Is it possible on API 19 to select a directory and then have write permission for everything beneath it
//TODO: Is it necessary to check if uri permissions are still in place? If it is make the user aware of the fact (red text or something)
public class SftpSettingsFragment
extends PluginSettingsFragment
implements StoragePreferenceDialogFragment.Callback,
Preference.OnPreferenceChangeListener,
StoragePreference.OnLongClickListener, ActionMode.Callback {
private final static String KEY_STORAGE_PREFERENCE_DIALOG = "StoragePreferenceDialog";
private final static String KEY_ACTION_MODE_STATE = "ActionModeState";
private final static String KEY_ACTION_MODE_ENABLED = "ActionModeEnabled";
private final static String KEY_ACTION_MODE_SELECTED_ITEMS = "ActionModeSelectedItems";
private List<SftpPlugin.StorageInfo> storageInfoList;
private PreferenceCategory preferenceCategory;
private ActionMode actionMode;
private JSONObject savedActionModeState;
public static SftpSettingsFragment newInstance(@NonNull String pluginKey) {
SftpSettingsFragment fragment = new SftpSettingsFragment();
fragment.setArguments(pluginKey);
return fragment;
}
public SftpSettingsFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
// super.onCreate creates PreferenceManager and calls onCreatePreferences()
super.onCreate(savedInstanceState);
if (getFragmentManager() != null) {
Fragment fragment = getFragmentManager().findFragmentByTag(KEY_STORAGE_PREFERENCE_DIALOG);
if (fragment != null) {
((StoragePreferenceDialogFragment) fragment).setCallback(this);
}
}
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_ACTION_MODE_STATE)) {
try {
savedActionModeState = new JSONObject(savedInstanceState.getString(KEY_ACTION_MODE_STATE, "{}"));
} catch (JSONException ignored) {}
}
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
TypedArray ta = requireContext().obtainStyledAttributes(new int[]{R.attr.colorAccent});
int colorAccent = ta.getColor(0, 0);
ta.recycle();
int sdkInt = Build.VERSION.SDK_INT;
storageInfoList = getStorageInfoList(requireContext());
PreferenceScreen preferenceScreen = getPreferenceScreen();
preferenceCategory = (PreferenceCategory) preferenceScreen
.findPreference(getString(R.string.sftp_preference_key_preference_category));
if (sdkInt <= 19) {
preferenceCategory.setTitle(R.string.sftp_preference_detected_sdcards);
} else {
preferenceCategory.setTitle(R.string.sftp_preference_configured_storage_locations);
}
addStoragePreferences(preferenceCategory);
Preference addStoragePreference = preferenceScreen.findPreference(getString(R.string.sftp_preference_key_add_storage));
addStoragePreference.getIcon().setColorFilter(colorAccent, PorterDuff.Mode.SRC_IN);
if (sdkInt <= 19) {
addStoragePreference.setVisible(false);
}
Preference addCameraShortcutPreference = preferenceScreen.findPreference(getString(R.string.sftp_preference_key_add_camera_shortcut));
if (sdkInt > 19) {
addCameraShortcutPreference.setVisible(false);
}
}
private void addStoragePreferences(PreferenceCategory preferenceCategory) {
/*
https://developer.android.com/guide/topics/ui/settings/programmatic-hierarchy
You can't just use any context to create Preferences, you have to use PreferenceManager's context
*/
Context context = getPreferenceManager().getContext();
sortStorageInfoListOnDisplayName();
for (int i = 0; i < storageInfoList.size(); i++) {
SftpPlugin.StorageInfo storageInfo = storageInfoList.get(i);
StoragePreference preference = new StoragePreference(context);
preference.setOnPreferenceChangeListener(this);
if (Build.VERSION.SDK_INT >= 21) {
preference.setOnLongClickListener(this);
}
preference.setKey(getString(R.string.sftp_preference_key_storage_info, i));
preference.setIcon(android.R.color.transparent);
preference.setDefaultValue(storageInfo);
if (storageInfo.isFileUri()) {
preference.setDialogTitle(R.string.sftp_preference_edit_sdcard_title);
} else {
preference.setDialogTitle(R.string.sftp_preference_edit_storage_location);
}
preferenceCategory.addPreference(preference);
}
}
@Override
protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
if (savedActionModeState != null) {
getListView().post(this::restoreActionMode);
}
return super.onCreateAdapter(preferenceScreen);
}
private void restoreActionMode() {
try {
if (savedActionModeState.getBoolean(KEY_ACTION_MODE_ENABLED)) {
actionMode = ((DeviceSettingsActivity)requireActivity()).startSupportActionMode(this);
if (actionMode != null) {
JSONArray jsonArray = savedActionModeState.getJSONArray(KEY_ACTION_MODE_SELECTED_ITEMS);
SparseBooleanArray selectedItems = new SparseBooleanArray();
for (int i = 0, count = jsonArray.length(); i < count; i++) {
selectedItems.put(jsonArray.getInt(i), true);
}
for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) {
StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i);
preference.setSelectable(true);
preference.checkbox.setChecked(selectedItems.get(i, false));
}
}
}
} catch (JSONException ignored) {}
}
@Override
public void onDisplayPreferenceDialog(Preference preference) {
if (preference instanceof StoragePreference) {
StoragePreferenceDialogFragment fragment = StoragePreferenceDialogFragment.newInstance(preference.getKey());
fragment.setTargetFragment(this, 0);
fragment.setCallback(this);
fragment.show(requireFragmentManager(), KEY_STORAGE_PREFERENCE_DIALOG);
} else {
super.onDisplayPreferenceDialog(preference);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put(KEY_ACTION_MODE_ENABLED, actionMode != null);
if (actionMode != null) {
JSONArray jsonArray = new JSONArray();
for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) {
StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i);
if (preference.checkbox.isChecked()) {
jsonArray.put(i);
}
}
jsonObject.put(KEY_ACTION_MODE_SELECTED_ITEMS, jsonArray);
}
outState.putString(KEY_ACTION_MODE_STATE, jsonObject.toString());
} catch (JSONException ignored) {}
}
private void saveStorageInfoList() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
JSONArray jsonArray = new JSONArray();
try {
for (SftpPlugin.StorageInfo storageInfo : storageInfoList) {
jsonArray.put(storageInfo.toJSON());
}
} catch (JSONException ignored) {}
preferences
.edit()
.putString(requireContext().getString(R.string.sftp_preference_key_storage_info_list), jsonArray.toString())
.apply();
}
@NonNull
static List<SftpPlugin.StorageInfo> getStorageInfoList(@NonNull Context context) {
ArrayList<SftpPlugin.StorageInfo> storageInfoList = new ArrayList<>();
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String jsonString = preferences
.getString(context.getString(R.string.sftp_preference_key_storage_info_list), "[]");
try {
JSONArray jsonArray = new JSONArray(jsonString);
for (int i = 0; i < jsonArray.length(); i++) {
storageInfoList.add(SftpPlugin.StorageInfo.fromJSON(jsonArray.getJSONObject(i)));
}
} catch (JSONException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT <= 19) {
addDetectedSDCardsToStorageInfoList(context, storageInfoList);
}
return storageInfoList;
}
private static void addDetectedSDCardsToStorageInfoList(@NonNull Context context, List<SftpPlugin.StorageInfo> storageInfoList) {
List<StorageHelper.StorageInfo> storageHelperInfoList = StorageHelper.getStorageList();
for (StorageHelper.StorageInfo info : storageHelperInfoList) {
// on at least API 17 emulator Environment.isExternalStorageRemovable returns false
if (info.removable || info.path.startsWith(Environment.getExternalStorageDirectory().getPath())) {
StringBuilder displayNameBuilder = new StringBuilder();
StringBuilder displayNameReadOnlyBuilder = new StringBuilder();
Uri sdCardUri = Uri.fromFile(new File(info.path));
if (isAlreadyConfigured(storageInfoList, sdCardUri)) {
continue;
}
int i = 1;
do {
if (i == 1) {
displayNameBuilder.append(context.getString(R.string.sftp_sdcard));
} else {
displayNameBuilder.setLength(0);
displayNameBuilder.append(context.getString(R.string.sftp_sdcard_num, i));
}
displayNameReadOnlyBuilder
.append(displayNameBuilder)
.append(" ")
.append(context.getString(R.string.sftp_readonly));
i++;
} while (!isDisplayNameUnique(storageInfoList, displayNameBuilder.toString(), displayNameReadOnlyBuilder.toString()));
String displayName = info.readonly ?
displayNameReadOnlyBuilder.toString() : displayNameBuilder.toString();
storageInfoList.add(new SftpPlugin.StorageInfo(displayName, Uri.fromFile(new File(info.path))));
}
}
}
private static boolean isDisplayNameUnique(List<SftpPlugin.StorageInfo> storageInfoList, String displayName, String displayNameReadOnly) {
for (SftpPlugin.StorageInfo info : storageInfoList) {
if (info.displayName.equals(displayName) || info.displayName.equals(displayName + displayNameReadOnly)) {
return false;
}
}
return true;
}
private static boolean isAlreadyConfigured(List<SftpPlugin.StorageInfo> storageInfoList, Uri sdCardUri) {
for (SftpPlugin.StorageInfo info : storageInfoList) {
if (info.uri.equals(sdCardUri)) {
return true;
}
}
return false;
}
private void sortStorageInfoListOnDisplayName() {
Collections.sort(storageInfoList, new SftpPlugin.StorageInfo.DisplayNameComparator());
}
@NonNull
@Override
public StoragePreferenceDialogFragment.CallbackResult isDisplayNameAllowed(@NonNull String displayName) {
StoragePreferenceDialogFragment.CallbackResult result = new StoragePreferenceDialogFragment.CallbackResult();
result.isAllowed = true;
if (displayName.isEmpty()) {
result.isAllowed = false;
result.errorMessage = getString(R.string.sftp_storage_preference_display_name_cannot_be_empty);
} else {
for (SftpPlugin.StorageInfo storageInfo : storageInfoList) {
if (storageInfo.displayName.equals(displayName)) {
result.isAllowed = false;
result.errorMessage = getString(R.string.sftp_storage_preference_display_name_already_used);
break;
}
}
}
return result;
}
@NonNull
@Override
public StoragePreferenceDialogFragment.CallbackResult isUriAllowed(@NonNull Uri uri) {
StoragePreferenceDialogFragment.CallbackResult result = new StoragePreferenceDialogFragment.CallbackResult();
result.isAllowed = true;
for (SftpPlugin.StorageInfo storageInfo : storageInfoList) {
if (storageInfo.uri.equals(uri)) {
result.isAllowed = false;
result.errorMessage = getString(R.string.sftp_storage_preference_storage_location_already_configured);
break;
}
}
return result;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void addNewStoragePreference(@NonNull SftpPlugin.StorageInfo storageInfo, int takeFlags) {
storageInfoList.add(storageInfo);
handleChangedStorageInfoList();
requireContext().getContentResolver().takePersistableUriPermission(storageInfo.uri, takeFlags);
}
private void handleChangedStorageInfoList() {
saveStorageInfoList();
preferenceCategory.removeAll();
addStoragePreferences(preferenceCategory);
Device device = BackgroundService.getInstance().getDevice(getDeviceId());
if (device != null) {
device.reloadPluginsFromSettings();
}
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
SftpPlugin.StorageInfo newStorageInfo = (SftpPlugin.StorageInfo) newValue;
ListIterator<SftpPlugin.StorageInfo> it = storageInfoList.listIterator();
while (it.hasNext()) {
SftpPlugin.StorageInfo storageInfo = it.next();
if (storageInfo.uri.equals(newStorageInfo.uri)) {
it.set(newStorageInfo);
break;
}
}
handleChangedStorageInfoList();
return false;
}
@Override
public void onLongClick(StoragePreference storagePreference) {
if (actionMode == null) {
actionMode = ((DeviceSettingsActivity)requireActivity()).startSupportActionMode(this);
if (actionMode != null) {
for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) {
StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i);
preference.setSelectable(true);
if (storagePreference.equals(preference)) {
preference.checkbox.setChecked(true);
}
}
}
}
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.sftp_settings_action_mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.delete:
for (int count = preferenceCategory.getPreferenceCount(), i = count - 1; i >= 0; i--) {
StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i);
if (preference.checkbox.isChecked()) {
SftpPlugin.StorageInfo info = storageInfoList.remove(i);
if (Build.VERSION.SDK_INT >= 21) {
requireContext().getContentResolver().releasePersistableUriPermission(info.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
}
actionMode.finish(); //This must be called before handleChangedStorageInfoList()
handleChangedStorageInfoList();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) {
StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i);
preference.setSelectable(false);
preference.checkbox.setChecked(false);
}
}
}

View File

@@ -21,15 +21,9 @@
package org.kde.kdeconnect.Plugins.SftpPlugin;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import org.apache.sshd.SshServer;
import org.apache.sshd.common.Session;
import org.apache.sshd.common.file.FileSystemFactory;
import org.apache.sshd.common.file.FileSystemView;
import org.apache.sshd.common.file.nativefs.NativeFileSystemView;
import org.apache.sshd.common.file.nativefs.NativeSshFile;
import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider;
import org.apache.sshd.common.util.SecurityUtils;
import org.apache.sshd.server.PasswordAuthenticator;
@@ -40,16 +34,10 @@ import org.apache.sshd.server.kex.DHG14;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.sftp.SftpSubsystem;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.MediaStoreHelper;
import org.kde.kdeconnect.Helpers.RandomHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
@@ -62,6 +50,7 @@ import java.security.Security;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
class SimpleSftpServer {
private static final int STARTPORT = 1739;
@@ -81,6 +70,7 @@ class SimpleSftpServer {
}
private final SshServer sshd = SshServer.setUpDefaultServer();
private AndroidFileSystemFactory fileSystemFactory;
void init(Context context, Device device) throws GeneralSecurityException {
@@ -100,7 +90,8 @@ class SimpleSftpServer {
}
});
sshd.setFileSystemFactory(new AndroidFileSystemFactory(context));
fileSystemFactory = new AndroidFileSystemFactory(context);
sshd.setFileSystemFactory(fileSystemFactory);
sshd.setCommandFactory(new ScpCommandFactory());
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystem.Factory()));
@@ -113,9 +104,9 @@ class SimpleSftpServer {
sshd.setPasswordAuthenticator(passwordAuth);
}
public boolean start() {
public boolean start(List<SftpPlugin.StorageInfo> storageInfoList) {
if (!started) {
fileSystemFactory.initRoots(storageInfoList);
passwordAuth.password = RandomHelper.randomString(28);
port = STARTPORT;
@@ -148,6 +139,10 @@ class SimpleSftpServer {
}
}
public boolean isStarted() {
return started;
}
String getPassword() {
return passwordAuth.password;
}
@@ -189,120 +184,6 @@ class SimpleSftpServer {
return ip6;
}
static class AndroidFileSystemFactory implements FileSystemFactory {
final private Context context;
AndroidFileSystemFactory(Context context) {
this.context = context;
}
@Override
public FileSystemView createFileSystemView(final Session username) {
return new AndroidFileSystemView(username.getUsername(), context);
}
}
static class AndroidFileSystemView extends NativeFileSystemView {
final private Context context;
AndroidFileSystemView(final String userName, Context context) {
super(userName, true);
this.context = context;
}
// NativeFileSystemView.getFile(), NativeSshFile.getParentFile() and NativeSshFile.listSshFiles() call
// createNativeSshFile to create new NativeSshFiles so override that instead of getFile() to always create a AndroidSshFile
@Override
public AndroidSshFile createNativeSshFile(String name, File file, String username) {
return new AndroidSshFile(this, name, file, username, context);
}
}
static class AndroidSshFile extends NativeSshFile {
final private Context context;
final private File file;
AndroidSshFile(final AndroidFileSystemView view, String name, final File file, final String userName, Context context) {
super(view, name, file, userName);
this.context = context;
this.file = file;
}
@Override
public OutputStream createOutputStream(long offset) throws IOException {
if (!isWritable()) {
throw new IOException("No write permission : " + file.getName());
}
final RandomAccessFile raf = new RandomAccessFile(file, "rw");
try {
if (offset < raf.length()) {
throw new IOException("Your SSHFS is bugged"); //SSHFS 3.0 and 3.2 cause data corruption, abort the transfer if this happens
}
raf.setLength(offset);
raf.seek(offset);
return new FileOutputStream(raf.getFD()) {
public void close() throws IOException {
super.close();
raf.close();
}
};
} catch (IOException e) {
raf.close();
throw e;
}
}
@Override
public boolean delete() {
//Log.e("Sshd", "deleting file");
boolean ret = super.delete();
if (ret) {
MediaStoreHelper.indexFile(context, Uri.fromFile(file));
}
return ret;
}
@Override
public boolean create() throws IOException {
//Log.e("Sshd", "creating file");
boolean ret = super.create();
if (ret) {
MediaStoreHelper.indexFile(context, Uri.fromFile(file));
}
return ret;
}
// Based on https://github.com/wolpi/prim-ftpd/blob/master/primitiveFTPd/src/org/primftpd/filesystem/FsFile.java
@Override
public boolean doesExist() {
boolean exists = file.exists();
if (!exists) {
// file.exists() returns false when we don't have read permission
// try to figure out if it really does not exist
File parentFile = file.getParentFile();
File[] children = parentFile.listFiles();
if (children != null) {
for (File child : children) {
if (file.equals(child)) {
exists = true;
break;
}
}
}
}
return exists;
}
}
static class SimplePasswordAuthenticator implements PasswordAuthenticator {
String password;

View File

@@ -0,0 +1,135 @@
/*
* 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 android.provider.DocumentsContract;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import org.kde.kdeconnect_tp.R;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceViewHolder;
import butterknife.BindView;
import butterknife.ButterKnife;
public class StoragePreference extends DialogPreference {
@Nullable
private SftpPlugin.StorageInfo storageInfo;
@Nullable
private OnLongClickListener onLongClickListener;
@BindView(R.id.checkbox) CheckBox checkbox;
public StoragePreference(Context context, AttributeSet attrs) {
super(context, attrs);
setDialogLayoutResource(R.layout.fragment_storage_preference_dialog);
setWidgetLayoutResource(R.layout.preference_checkbox);
setPersistent(false);
setSelectable(false);
}
public StoragePreference(Context context) {
this(context, null);
}
public void setOnLongClickListener(@Nullable OnLongClickListener listener) {
this.onLongClickListener = listener;
}
public void setStorageInfo(@NonNull SftpPlugin.StorageInfo storageInfo) {
if (this.storageInfo != null && this.storageInfo.equals(storageInfo)) {
return;
}
if (callChangeListener(storageInfo)) {
setStorageInfoInternal(storageInfo);
}
}
private void setStorageInfoInternal(@NonNull SftpPlugin.StorageInfo storageInfo) {
this.storageInfo = storageInfo;
setTitle(storageInfo.displayName);
if (Build.VERSION.SDK_INT < 21) {
setSummary(storageInfo.uri.getPath());
} else {
setSummary(DocumentsContract.getTreeDocumentId(storageInfo.uri));
}
}
@Nullable
public SftpPlugin.StorageInfo getStorageInfo() {
return storageInfo;
}
@Override
public void setDefaultValue(Object defaultValue) {
if (defaultValue == null || defaultValue instanceof SftpPlugin.StorageInfo) {
super.setDefaultValue(defaultValue);
} else {
throw new RuntimeException("StoragePreference defaultValue must be null or an instance of StfpPlugin.StorageInfo");
}
}
@Override
protected void onSetInitialValue(@Nullable Object defaultValue) {
if (defaultValue != null) {
setStorageInfoInternal((SftpPlugin.StorageInfo) defaultValue);
}
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
ButterKnife.bind(this, holder.itemView);
checkbox.setVisibility(isSelectable() ? View.VISIBLE : View.INVISIBLE);
holder.itemView.setOnLongClickListener(v -> {
if (onLongClickListener != null) {
onLongClickListener.onLongClick(StoragePreference.this);
return true;
}
return false;
});
}
@Override
protected void onClick() {
if (isSelectable()) {
checkbox.setChecked(!checkbox.isChecked());
} else {
super.onClick();
}
}
public interface OnLongClickListener {
void onLongClick(StoragePreference storagePreference);
}
}

View File

@@ -0,0 +1,330 @@
package org.kde.kdeconnect.Plugins.SftpPlugin;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.text.Editable;
import android.text.InputFilter;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.Helpers.StorageHelper;
import org.kde.kdeconnect_tp.R;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.preference.PreferenceDialogFragmentCompat;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
public class StoragePreferenceDialogFragment extends PreferenceDialogFragmentCompat implements TextWatcher {
private static final int REQUEST_CODE_DOCUMENT_TREE = 1001;
//When state is restored I cannot determine if an error is going to be displayed on one of the TextInputEditText's or not so I have to remember if the dialog's positive button was enabled or not
private static final String KEY_POSITIVE_BUTTON_ENABLED = "PositiveButtonEnabled";
private static final String KEY_STORAGE_INFO = "StorageInfo";
private static final String KEY_TAKE_FLAGS = "TakeFlags";
@BindView(R.id.storageLocation) TextInputEditText storageLocation;
@BindView(R.id.storageDisplayName) TextInputEditText storageDisplayName;
@BindView(R.id.storageDisplayNameInputLayout) TextInputLayout storageDisplayInputLayout;
private Unbinder unbinder;
private Callback callback;
private Drawable arrowDropDownDrawable;
private Button positiveButton;
private boolean stateRestored;
private boolean enablePositiveButton;
private SftpPlugin.StorageInfo storageInfo;
private int takeFlags;
public static StoragePreferenceDialogFragment newInstance(String key) {
StoragePreferenceDialogFragment fragment = new StoragePreferenceDialogFragment();
Bundle args = new Bundle();
args.putString(ARG_KEY, key);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
stateRestored = false;
enablePositiveButton = true;
if (savedInstanceState != null) {
stateRestored = true;
enablePositiveButton = savedInstanceState.getBoolean(KEY_POSITIVE_BUTTON_ENABLED);
takeFlags = savedInstanceState.getInt(KEY_TAKE_FLAGS, 0);
try {
JSONObject jsonObject = new JSONObject(savedInstanceState.getString(KEY_STORAGE_INFO, "{}"));
storageInfo = SftpPlugin.StorageInfo.fromJSON(jsonObject);
} catch (JSONException ignored) {}
}
Drawable drawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_arrow_drop_down_24px);
if (drawable != null) {
drawable = DrawableCompat.wrap(drawable);
DrawableCompat.setTint(drawable, getResources().getColor(android.R.color.darker_gray));
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
arrowDropDownDrawable = drawable;
}
}
void setCallback(Callback callback) {
this.callback = callback;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog dialog = (AlertDialog) super.onCreateDialog(savedInstanceState);
dialog.setOnShowListener(dialog1 -> {
AlertDialog alertDialog = (AlertDialog) dialog1;
positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setEnabled(enablePositiveButton);
});
return dialog;
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
unbinder = ButterKnife.bind(this, view);
storageDisplayName.setFilters(new InputFilter[]{new FileSeparatorCharFilter()});
storageDisplayName.addTextChangedListener(this);
if (getPreference().getKey().equals(getString(R.string.sftp_preference_key_add_storage))) {
if (!stateRestored) {
enablePositiveButton = false;
storageLocation.setText(requireContext().getString(R.string.sftp_storage_preference_click_to_select));
}
boolean isClickToSelect = storageLocation.getText() != null && storageLocation.getText().toString().equals(getString(R.string.sftp_storage_preference_click_to_select));
storageLocation.setCompoundDrawables(null, null, isClickToSelect ? arrowDropDownDrawable : null, null);
storageLocation.setEnabled(isClickToSelect);
storageLocation.setFocusable(false);
storageLocation.setFocusableInTouchMode(false);
storageDisplayName.setEnabled(!isClickToSelect);
} else {
if (!stateRestored) {
StoragePreference preference = (StoragePreference) getPreference();
SftpPlugin.StorageInfo info = preference.getStorageInfo();
if (info == null) {
throw new RuntimeException("Cannot edit a StoragePreference that does not have its storageInfo set");
}
storageInfo = SftpPlugin.StorageInfo.copy(info);
if (Build.VERSION.SDK_INT < 21) {
storageLocation.setText(storageInfo.uri.getPath());
} else {
storageLocation.setText(DocumentsContract.getTreeDocumentId(storageInfo.uri));
}
storageDisplayName.setText(storageInfo.displayName);
}
storageLocation.setCompoundDrawables(null, null, null, null);
storageLocation.setEnabled(false);
storageLocation.setFocusable(false);
storageLocation.setFocusableInTouchMode(false);
storageDisplayName.setEnabled(true);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@OnClick(R.id.storageLocation)
void onSelectStorageClicked() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
//For API >= 26 we can also set Extra: DocumentsContract.EXTRA_INITIAL_URI
startActivityForResult(intent, REQUEST_CODE_DOCUMENT_TREE);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != Activity.RESULT_OK) {
return;
}
switch (requestCode) {
case REQUEST_CODE_DOCUMENT_TREE:
Uri uri = data.getData();
takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (uri == null) {
return;
}
CallbackResult result = callback.isUriAllowed(uri);
if (result.isAllowed) {
String documentId = DocumentsContract.getTreeDocumentId(uri);
String displayName = StorageHelper.getDisplayName(requireContext(), uri);
storageInfo = new SftpPlugin.StorageInfo(displayName, uri);
storageLocation.setText(documentId);
storageLocation.setCompoundDrawables(null, null, null, null);
storageLocation.setError(null);
storageLocation.setEnabled(false);
// TODO: Show name as used in android's picker app but I don't think it's possible to get that, everything I tried throws PermissionDeniedException
storageDisplayName.setText(displayName);
storageDisplayName.setEnabled(true);
} else {
storageLocation.setError(result.errorMessage);
setPositiveButtonEnabled(false);
}
break;
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_POSITIVE_BUTTON_ENABLED, positiveButton.isEnabled());
outState.putInt(KEY_TAKE_FLAGS, takeFlags);
if (storageInfo != null) {
try {
outState.putString(KEY_STORAGE_INFO, storageInfo.toJSON().toString());
} catch (JSONException ignored) {}
}
}
@Override
public void onDialogClosed(boolean positiveResult) {
if (positiveResult) {
storageInfo.displayName = storageDisplayName.getText().toString();
if (getPreference().getKey().equals(getString(R.string.sftp_preference_key_add_storage))) {
callback.addNewStoragePreference(storageInfo, takeFlags);
} else {
((StoragePreference)getPreference()).setStorageInfo(storageInfo);
}
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
//Don't care
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//Don't care
}
@Override
public void afterTextChanged(Editable s) {
String displayName = s.toString();
StoragePreference storagePreference = (StoragePreference) getPreference();
SftpPlugin.StorageInfo storageInfo = storagePreference.getStorageInfo();
if (storageInfo == null || !storageInfo.displayName.equals(displayName)) {
CallbackResult result = callback.isDisplayNameAllowed(displayName);
if (result.isAllowed) {
setPositiveButtonEnabled(true);
} else {
setPositiveButtonEnabled(false);
storageDisplayName.setError(result.errorMessage);
}
}
}
private void setPositiveButtonEnabled(boolean enabled) {
if (positiveButton != null) {
positiveButton.setEnabled(enabled);
} else {
enablePositiveButton = enabled;
}
}
private class FileSeparatorCharFilter implements InputFilter {
//TODO: Add more chars to refuse?
//https://www.cyberciti.biz/faq/linuxunix-rules-for-naming-file-and-directory-names/
String notAllowed = "/\\><|:&?*";
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
boolean keepOriginal = true;
StringBuilder sb = new StringBuilder(end - start);
for (int i = start; i < end; i++) {
char c = source.charAt(i);
if (notAllowed.indexOf(c) < 0) {
sb.append(c);
} else {
keepOriginal = false;
sb.append("_");
}
}
if (keepOriginal) {
return null;
} else {
if (source instanceof Spanned) {
SpannableString sp = new SpannableString(sb);
TextUtils.copySpansFrom((Spanned) source, start, sb.length(), null, sp, 0);
return sp;
} else {
return sb;
}
}
}
}
static class CallbackResult {
boolean isAllowed;
String errorMessage;
}
interface Callback {
@NonNull CallbackResult isDisplayNameAllowed(@NonNull String displayName);
@NonNull CallbackResult isUriAllowed(@NonNull Uri uri);
void addNewStoragePreference(@NonNull SftpPlugin.StorageInfo storageInfo, int takeFlags);
}
}

View File

@@ -25,14 +25,13 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.FilesHelper;
import org.kde.kdeconnect.Helpers.MediaStoreHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.async.BackgroundJob;
import org.kde.kdeconnect_tp.R;
import java.io.BufferedOutputStream;
@@ -46,13 +45,7 @@ import java.util.List;
import androidx.core.content.FileProvider;
import androidx.documentfile.provider.DocumentFile;
class CompositeReceiveFileRunnable implements Runnable {
interface CallBack {
void onSuccess(CompositeReceiveFileRunnable runnable);
void onError(CompositeReceiveFileRunnable runnable, Throwable error);
}
private final Device device;
public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
private final ShareNotification shareNotification;
private NetworkPacket currentNetworkPacket;
private String currentFileName;
@@ -61,29 +54,29 @@ class CompositeReceiveFileRunnable implements Runnable {
private long lastProgressTimeMillis;
private long prevProgressPercentage;
private final CallBack callBack;
private final Handler handler;
private final Object lock; //Use to protect concurrent access to the variables below
private final List<NetworkPacket> networkPacketList;
private int totalNumFiles;
private long totalPayloadSize;
private boolean isRunning;
CompositeReceiveFileRunnable(Device device, CallBack callBack) {
this.device = device;
this.callBack = callBack;
CompositeReceiveFileJob(Device device, BackgroundJob.Callback<Void> callBack) {
super(device, callBack);
lock = new Object();
networkPacketList = new ArrayList<>();
shareNotification = new ShareNotification(device);
shareNotification.addCancelAction(getId());
currentFileNum = 0;
totalNumFiles = 0;
totalPayloadSize = 0;
totalReceived = 0;
lastProgressTimeMillis = 0;
prevProgressPercentage = 0;
handler = new Handler(Looper.getMainLooper());
}
private Device getDevice() {
return requestInfo;
}
boolean isRunning() { return isRunning; }
@@ -93,8 +86,8 @@ class CompositeReceiveFileRunnable implements Runnable {
this.totalNumFiles = numberOfFiles;
this.totalPayloadSize = totalPayloadSize;
shareNotification.setTitle(device.getContext().getResources()
.getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, device.getName()));
shareNotification.setTitle(getDevice().getContext().getResources()
.getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, getDevice().getName()));
}
}
@@ -106,8 +99,8 @@ class CompositeReceiveFileRunnable implements Runnable {
totalNumFiles = networkPacket.getInt(SharePlugin.KEY_NUMBER_OF_FILES, 1);
totalPayloadSize = networkPacket.getLong(SharePlugin.KEY_TOTAL_PAYLOAD_SIZE);
shareNotification.setTitle(device.getContext().getResources()
.getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, device.getName()));
shareNotification.setTitle(getDevice().getContext().getResources()
.getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, getDevice().getName()));
}
}
}
@@ -126,7 +119,7 @@ class CompositeReceiveFileRunnable implements Runnable {
isRunning = true;
while (!done) {
while (!done && !canceled) {
synchronized (lock) {
currentNetworkPacket = networkPacketList.get(0);
}
@@ -138,7 +131,7 @@ class CompositeReceiveFileRunnable implements Runnable {
fileDocument = getDocumentFileFor(currentFileName, currentNetworkPacket.getBoolean("open"));
if (currentNetworkPacket.hasPayload()) {
outputStream = new BufferedOutputStream(device.getContext().getContentResolver().openOutputStream(fileDocument.getUri()));
outputStream = new BufferedOutputStream(getDevice().getContext().getContentResolver().openOutputStream(fileDocument.getUri()));
InputStream inputStream = currentNetworkPacket.getPayload().getInputStream();
long received = receiveFile(inputStream, outputStream);
@@ -147,7 +140,10 @@ class CompositeReceiveFileRunnable implements Runnable {
if ( received != currentNetworkPacket.getPayloadSize()) {
fileDocument.delete();
throw new RuntimeException("Failed to receive: " + currentFileName + " received:" + received + " bytes, expected: " + currentNetworkPacket.getPayloadSize() + " bytes");
if (!canceled) {
throw new RuntimeException("Failed to receive: " + currentFileName + " received:" + received + " bytes, expected: " + currentNetworkPacket.getPayloadSize() + " bytes");
}
} else {
publishFile(fileDocument, received);
}
@@ -163,7 +159,7 @@ class CompositeReceiveFileRunnable implements Runnable {
listIsEmpty = networkPacketList.isEmpty();
}
if (listIsEmpty) {
if (listIsEmpty && !canceled) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
@@ -182,6 +178,11 @@ class CompositeReceiveFileRunnable implements Runnable {
isRunning = false;
if (canceled) {
shareNotification.cancel();
return;
}
int numFiles;
synchronized (lock) {
numFiles = totalNumFiles;
@@ -192,7 +193,7 @@ class CompositeReceiveFileRunnable implements Runnable {
openFile(fileDocument);
} else {
//Update the notification and allow to open the file from it
shareNotification.setFinished(device.getContext().getResources().getQuantityString(R.plurals.received_files_title, numFiles, device.getName(), numFiles));
shareNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_title, numFiles, getDevice().getName(), numFiles));
if (totalNumFiles == 1 && fileDocument != null) {
shareNotification.setURI(fileDocument.getUri(), fileDocument.getType(), fileDocument.getName());
@@ -200,7 +201,7 @@ class CompositeReceiveFileRunnable implements Runnable {
shareNotification.show();
}
handler.post(() -> callBack.onSuccess(this));
reportResult(null);
} catch (Exception e) {
isRunning = false;
@@ -208,9 +209,10 @@ class CompositeReceiveFileRunnable implements Runnable {
synchronized (lock) {
failedFiles = (totalNumFiles - currentFileNum + 1);
}
shareNotification.setFinished(device.getContext().getResources().getQuantityString(R.plurals.received_files_fail_title, failedFiles, device.getName(), failedFiles, totalNumFiles));
shareNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_fail_title, failedFiles, getDevice().getName(), failedFiles, totalNumFiles));
shareNotification.show();
handler.post(() -> callBack.onError(this, e));
reportError(e);
} finally {
closeAllInputStreams();
networkPacketList.clear();
@@ -230,12 +232,12 @@ class CompositeReceiveFileRunnable implements Runnable {
//We need to check for already existing files only when storing in the default path.
//User-defined paths use the new Storage Access Framework that already handles this.
//If the file should be opened immediately store it in the standard location to avoid the FileProvider trouble (See ShareNotification::setURI)
if (open || !ShareSettingsFragment.isCustomDestinationEnabled(device.getContext())) {
if (open || !ShareSettingsFragment.isCustomDestinationEnabled(getDevice().getContext())) {
final String defaultPath = ShareSettingsFragment.getDefaultDestinationDirectory().getAbsolutePath();
filenameToUse = FilesHelper.findNonExistingNameForNewFile(defaultPath, filenameToUse);
destinationFolderDocument = DocumentFile.fromFile(new File(defaultPath));
} else {
destinationFolderDocument = ShareSettingsFragment.getDestinationDirectory(device.getContext());
destinationFolderDocument = ShareSettingsFragment.getDestinationDirectory(getDevice().getContext());
}
String displayName = FilesHelper.getFileNameWithoutExt(filenameToUse);
String mimeType = FilesHelper.getMimeTypeFromFile(filenameToUse);
@@ -247,7 +249,7 @@ class CompositeReceiveFileRunnable implements Runnable {
DocumentFile fileDocument = destinationFolderDocument.createFile(mimeType, displayName);
if (fileDocument == null) {
throw new RuntimeException(device.getContext().getString(R.string.cannot_create_file, filenameToUse));
throw new RuntimeException(getDevice().getContext().getString(R.string.cannot_create_file, filenameToUse));
}
return fileDocument;
@@ -258,7 +260,7 @@ class CompositeReceiveFileRunnable implements Runnable {
int count;
long received = 0;
while ((count = input.read(data)) >= 0) {
while ((count = input.read(data)) >= 0 && !canceled) {
received += count;
totalReceived += count;
@@ -291,21 +293,21 @@ class CompositeReceiveFileRunnable implements Runnable {
private void setProgress(int progress) {
synchronized (lock) {
shareNotification.setProgress(progress, device.getContext().getResources()
shareNotification.setProgress(progress, getDevice().getContext().getResources()
.getQuantityString(R.plurals.incoming_files_text, totalNumFiles, currentFileName, currentFileNum, totalNumFiles));
}
shareNotification.show();
}
private void publishFile(DocumentFile fileDocument, long size) {
if (!ShareSettingsFragment.isCustomDestinationEnabled(device.getContext())) {
if (!ShareSettingsFragment.isCustomDestinationEnabled(getDevice().getContext())) {
Log.i("SharePlugin", "Adding to downloads");
DownloadManager manager = (DownloadManager) device.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
manager.addCompletedDownload(fileDocument.getUri().getLastPathSegment(), device.getName(), true, fileDocument.getType(), fileDocument.getUri().getPath(), size, false);
DownloadManager manager = (DownloadManager) getDevice().getContext().getSystemService(Context.DOWNLOAD_SERVICE);
manager.addCompletedDownload(fileDocument.getUri().getLastPathSegment(), getDevice().getName(), true, fileDocument.getType(), fileDocument.getUri().getPath(), size, false);
} else {
//Make sure it is added to the Android Gallery anyway
Log.i("SharePlugin", "Adding to gallery");
MediaStoreHelper.indexFile(device.getContext(), fileDocument.getUri());
MediaStoreHelper.indexFile(getDevice().getContext(), fileDocument.getUri());
}
}
@@ -315,13 +317,13 @@ class CompositeReceiveFileRunnable implements Runnable {
if (Build.VERSION.SDK_INT >= 24) {
//Nougat and later require "content://" uris instead of "file://" uris
File file = new File(fileDocument.getUri().getPath());
Uri contentUri = FileProvider.getUriForFile(device.getContext(), "org.kde.kdeconnect_tp.fileprovider", file);
Uri contentUri = FileProvider.getUriForFile(getDevice().getContext(), "org.kde.kdeconnect_tp.fileprovider", file);
intent.setDataAndType(contentUri, mimeType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
intent.setDataAndType(fileDocument.getUri(), mimeType);
}
device.getContext().startActivity(intent);
getDevice().getContext().startActivity(intent);
}
}

View File

@@ -89,7 +89,7 @@ public class SendFileActivity extends AppCompatActivity {
if (uris.isEmpty()) {
Log.w("SendFileActivity", "No files to send?");
} else {
BackgroundService.runWithPlugin(this, mDeviceId, SharePlugin.class, plugin -> plugin.queuedSendUriList(uris));
BackgroundService.RunWithPlugin(this, mDeviceId, SharePlugin.class, plugin -> plugin.queuedSendUriList(uris));
}
}
finish();

View File

@@ -113,7 +113,7 @@ public class ShareActivity extends AppCompatActivity {
list.setOnItemClickListener((adapterView, view, i, l) -> {
Device device = devicesList.get(i - 1); //NOTE: -1 because of the title!
BackgroundService.runWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent));
BackgroundService.RunWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent));
finish();
});
});
@@ -147,8 +147,7 @@ public class ShareActivity extends AppCompatActivity {
final String deviceId = intent.getStringExtra("deviceId");
if (deviceId != null) {
BackgroundService.runWithPlugin(this, deviceId, SharePlugin.class, plugin -> {
BackgroundService.RunWithPlugin(this, deviceId, SharePlugin.class, plugin -> {
plugin.share(intent);
finish();
});

View File

@@ -0,0 +1,57 @@
/*
* 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.SharePlugin;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.kde.kdeconnect.BackgroundService;
public class ShareBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case SharePlugin.ACTION_CANCEL_SHARE:
cancelShare(context, intent);
break;
default:
Log.d("ShareBroadcastReceiver", "Unhandled Action received: " + intent.getAction());
}
}
private void cancelShare(Context context, Intent intent) {
if (!intent.hasExtra(SharePlugin.CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA) ||
!intent.hasExtra(SharePlugin.CANCEL_SHARE_DEVICE_ID_EXTRA)) {
Log.e("ShareBroadcastReceiver", "cancelShare() - not all expected extra's are present. Ignoring this cancel intent");
return;
}
long jobId = intent.getLongExtra(SharePlugin.CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA, -1);
String deviceId = intent.getStringExtra(SharePlugin.CANCEL_SHARE_DEVICE_ID_EXTRA);
BackgroundService.RunCommand(context, service -> {
SharePlugin plugin = service.getDevice(deviceId).getPlugin(SharePlugin.class);
plugin.cancelJob(jobId);
});
}
}

View File

@@ -48,6 +48,7 @@ class ShareNotification {
private final int notificationId;
private NotificationCompat.Builder builder;
private final Device device;
private long currentJobId;
//https://documentation.onesignal.com/docs/android-customizations#section-big-picture
private static final int bigImageWidth = 1440;
@@ -73,7 +74,23 @@ class ShareNotification {
notificationManager.cancel(notificationId);
}
public int getId() {
public void addCancelAction(long jobId) {
builder.mActions.clear();
currentJobId = jobId;
Intent cancelIntent = new Intent(device.getContext(), ShareBroadcastReceiver.class);
cancelIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
cancelIntent.setAction(SharePlugin.ACTION_CANCEL_SHARE);
cancelIntent.putExtra(SharePlugin.CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA, jobId);
cancelIntent.putExtra(SharePlugin.CANCEL_SHARE_DEVICE_ID_EXTRA, device.getDeviceId());
PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(device.getContext(), 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.addAction(R.drawable.ic_reject_pairing, device.getContext().getString(R.string.cancel), cancelPendingIntent);
}
public long getCurrentJobId() { return currentJobId; }
public int getNotificationId() {
return notificationId;
}

View File

@@ -26,17 +26,14 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
@@ -46,37 +43,40 @@ import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
import org.kde.kdeconnect.async.BackgroundJob;
import org.kde.kdeconnect.async.BackgroundJobHandler;
import org.kde.kdeconnect_tp.R;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
@PluginFactory.LoadablePlugin
public class SharePlugin extends Plugin {
final static String ACTION_CANCEL_SHARE = "org.kde.kdeconnect.Plugins.SharePlugin.CancelShare";
final static String CANCEL_SHARE_DEVICE_ID_EXTRA = "deviceId";
final static String CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA = "backgroundJobId";
private final static String PACKET_TYPE_SHARE_REQUEST = "kdeconnect.share.request";
private final static String PACKET_TYPE_SHARE_REQUEST_UPDATE = "kdeconnect.share.request.update";
final static String KEY_NUMBER_OF_FILES = "numberOfFiles";
final static String KEY_TOTAL_PAYLOAD_SIZE = "totalPayloadSize";
private final static boolean openUrlsDirectly = true;
private ExecutorService executorService;
private BackgroundJobHandler backgroundJobHandler;
private final Handler handler;
private CompositeReceiveFileRunnable receiveFileRunnable;
private final Callback receiveFileRunnableCallback;
private CompositeReceiveFileJob receiveFileJob;
private final Callback receiveFileJobCallback;
public SharePlugin() {
executorService = Executors.newFixedThreadPool(5);
backgroundJobHandler = BackgroundJobHandler.newFixedThreadPoolBackgroundJobHander(5);
handler = new Handler(Looper.getMainLooper());
receiveFileRunnableCallback = new Callback();
receiveFileJobCallback = new Callback();
}
@Override
@@ -127,8 +127,8 @@ public class SharePlugin extends Plugin {
public boolean onPacketReceived(NetworkPacket np) {
try {
if (np.getType().equals(PACKET_TYPE_SHARE_REQUEST_UPDATE)) {
if (receiveFileRunnable != null && receiveFileRunnable.isRunning()) {
receiveFileRunnable.updateTotals(np.getInt(KEY_NUMBER_OF_FILES), np.getLong(KEY_TOTAL_PAYLOAD_SIZE));
if (receiveFileJob != null && receiveFileJob.isRunning()) {
receiveFileJob.updateTotals(np.getInt(KEY_NUMBER_OF_FILES), np.getLong(KEY_TOTAL_PAYLOAD_SIZE));
} else {
Log.d("SharePlugin", "Received update packet but CompositeUploadJob is null or not running");
}
@@ -168,32 +168,7 @@ public class SharePlugin extends Plugin {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (openUrlsDirectly) {
context.startActivity(browserIntent);
} else {
Resources res = context.getResources();
PendingIntent resultPendingIntent = PendingIntent.getActivity(
context,
0,
browserIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification noti = new NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)
.setContentTitle(res.getString(R.string.received_url_title, device.getName()))
.setContentText(res.getString(R.string.received_url_text, url))
.setContentIntent(resultPendingIntent)
.setTicker(res.getString(R.string.received_url_title, device.getName()))
.setSmallIcon(R.drawable.ic_notification)
.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_ALL)
.build();
NotificationHelper.notifyCompat(notificationManager, (int) System.currentTimeMillis(), noti);
}
context.startActivity(browserIntent);
}
private void receiveText(NetworkPacket np) {
@@ -205,15 +180,15 @@ public class SharePlugin extends Plugin {
@WorkerThread
private void receiveFile(NetworkPacket np) {
CompositeReceiveFileRunnable runnable;
CompositeReceiveFileJob job;
boolean hasNumberOfFiles = np.has(KEY_NUMBER_OF_FILES);
boolean hasOpen = np.has("open");
if (hasNumberOfFiles && !hasOpen && receiveFileRunnable != null) {
runnable = receiveFileRunnable;
if (hasNumberOfFiles && !hasOpen && receiveFileJob != null) {
job = receiveFileJob;
} else {
runnable = new CompositeReceiveFileRunnable(device, receiveFileRunnableCallback);
job = new CompositeReceiveFileJob(device, receiveFileJobCallback);
}
if (!hasNumberOfFiles) {
@@ -221,13 +196,13 @@ public class SharePlugin extends Plugin {
np.set(KEY_TOTAL_PAYLOAD_SIZE, np.getPayloadSize());
}
runnable.addNetworkPacket(np);
job.addNetworkPacket(np);
if (runnable != receiveFileRunnable) {
if (job != receiveFileJob) {
if (hasNumberOfFiles && !hasOpen) {
receiveFileRunnable = runnable;
receiveFileJob = job;
}
executorService.execute(runnable);
backgroundJobHandler.runJob(job);
}
}
@@ -237,7 +212,6 @@ public class SharePlugin extends Plugin {
}
void queuedSendUriList(final ArrayList<Uri> uriList) {
//Read all the data early, as we only have permissions to do it while the activity is alive
final ArrayList<NetworkPacket> toSend = new ArrayList<>();
for (Uri uri : uriList) {
@@ -286,7 +260,6 @@ public class SharePlugin extends Plugin {
}
queuedSendUriList(uriList);
} catch (Exception e) {
Log.e("ShareActivity", "Exception");
e.printStackTrace();
@@ -320,7 +293,6 @@ public class SharePlugin extends Plugin {
device.sendPacket(np);
}
}
}
@Override
@@ -338,19 +310,32 @@ public class SharePlugin extends Plugin {
return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
}
private class Callback implements CompositeReceiveFileRunnable.CallBack {
private class Callback implements CompositeReceiveFileJob.Callback<Void> {
@Override
public void onSuccess(CompositeReceiveFileRunnable runnable) {
if (runnable == receiveFileRunnable) {
receiveFileRunnable = null;
public void onResult(@NonNull BackgroundJob job, Void result) {
if (job == receiveFileJob) {
receiveFileJob = null;
}
}
@Override
public void onError(CompositeReceiveFileRunnable runnable, Throwable error) {
Log.e("SharePlugin", "onError() - " + error.getMessage());
if (runnable == receiveFileRunnable) {
receiveFileRunnable = null;
public void onError(@NonNull BackgroundJob job, @NonNull Throwable error) {
if (job == receiveFileJob) {
receiveFileJob = null;
}
}
}
void cancelJob(long jobId) {
if (backgroundJobHandler.isRunning(jobId)) {
BackgroundJob job = backgroundJobHandler.getJob(jobId);
if (job != null) {
job.cancel();
if (job == receiveFileJob) {
receiveFileJob = null;
}
}
}
}

View File

@@ -30,10 +30,10 @@ import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
@PluginFactory.LoadablePlugin
public class SystemvolumePlugin extends Plugin {
public class SystemVolumePlugin extends Plugin {
private final static String PACKET_TYPE_SYSTEMVOLUME = "kdeconnect.systemvolume";
private final static String PACKET_TYPE_SYSTEMVOLUME_REQUEST = "kdeconnect.systemvolume.request";
@@ -42,11 +42,11 @@ public class SystemvolumePlugin extends Plugin {
void sinksChanged();
}
private final HashMap<String, Sink> sinks;
private final ConcurrentHashMap<String, Sink> sinks;
private final ArrayList<SinkListener> listeners;
public SystemvolumePlugin() {
sinks = new HashMap<>();
public SystemVolumePlugin() {
sinks = new ConcurrentHashMap<>();
listeners = new ArrayList<>();
}

View File

@@ -37,9 +37,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;
public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemvolumePlugin.SinkListener {
public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemVolumePlugin.SinkListener {
private SystemvolumePlugin plugin;
private SystemVolumePlugin plugin;
private Activity activity;
private SinkAdapter adapter;
private Context context;
@@ -63,7 +63,7 @@ public class SystemvolumeFragment extends ListFragment implements Sink.UpdateLis
}
public void connectToPlugin(final String deviceId) {
BackgroundService.runWithPlugin(activity, deviceId, SystemvolumePlugin.class, plugin -> {
BackgroundService.RunWithPlugin(activity, deviceId, SystemVolumePlugin.class, plugin -> {
this.plugin = plugin;
plugin.addSinkListener(SystemvolumeFragment.this);
plugin.requestSinkList();

View File

@@ -51,14 +51,11 @@ public class TelephonyPlugin extends Plugin {
/**
* Packet used for simple telephony events
* Packet used for simple call events
* <p>
* It contains the key "event" which maps to a string indicating the type of event:
* - "ringing" - A phone call is incoming
* - "missedCall" - An incoming call was not answered
* - "sms" - An incoming SMS message
* - Note: As of this writing (15 May 2018) the SMS interface is being improved and this type of event
* is no longer the preferred way of handling SMS. Use the packets defined by the SMS plugin instead.
* <p>
* Depending on the event, other fields may be defined
*/
@@ -70,18 +67,9 @@ public class TelephonyPlugin extends Plugin {
* The two possible events used the be to request a message be sent or request the device
* silence its ringer
* <p>
* In case an SMS was being requested, the body was like so:
* { "sendSms": true,
* "phoneNumber": "542904563213",
* "messageBody": "Hi mom!"
* }
* <p>
* In case a ringer muted was requested, the body looked like so:
* { "action": "mute" }
* <p>
* As of 15 May 2018, the SMS interface is being improved. Use the packets defined by the
* SMS plugin instead for SMS events
* <p>
* Ringer mute requests are best handled by PACKET_TYPE_TELEPHONY_REQUEST_MUTE
* <p>
* This packet type is retained for backwards-compatibility with old desktop applications,
@@ -255,9 +243,8 @@ public class TelephonyPlugin extends Plugin {
@Override
public boolean onCreate() {
IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
IntentFilter filter = new IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
filter.setPriority(500);
filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
context.registerReceiver(receiver, filter);
permissionExplanation = R.string.telephony_permission_explanation;
optionalPermissionExplanation = R.string.telephony_optional_permission_explanation;
@@ -314,7 +301,15 @@ public class TelephonyPlugin extends Plugin {
@Override
public String[] getRequiredPermissions() {
return new String[]{Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_SMS};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return new String[]{
Manifest.permission.READ_PHONE_STATE,
//FIXME: Disabled because of https://support.google.com/googleplay/android-developer/answer/9047303
//Manifest.permission.READ_CALL_LOG
};
} else {
return new String[0];
}
}
@Override

View File

@@ -15,8 +15,8 @@
* 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/>.
*/
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.UserInterface;
@@ -131,4 +131,8 @@ public class DeviceSettingsActivity
public void onFinish() {
finish();
}
public String getDeviceId() {
return deviceId;
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2019 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.UserInterface;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class DeviceSettingsAlertDialogFragment extends AlertDialogFragment {
private static final String KEY_PLUGIN_KEY = "PluginKey";
private static final String KEY_DEVICE_ID = "DeviceId";
private String pluginKey;
private String deviceId;
public DeviceSettingsAlertDialogFragment() {}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args == null || !args.containsKey(KEY_PLUGIN_KEY)) {
throw new RuntimeException("You must call Builder.setPluginKey() to set the plugin");
}
if (!args.containsKey(KEY_DEVICE_ID)) {
throw new RuntimeException("You must call Builder.setDeviceId() to set the device");
}
pluginKey = args.getString(KEY_PLUGIN_KEY);
deviceId = args.getString(KEY_DEVICE_ID);
setCallback(new Callback() {
@Override
public void onPositiveButtonClicked() {
Intent intent = new Intent(requireActivity(), DeviceSettingsActivity.class);
intent.putExtra(DeviceSettingsActivity.EXTRA_DEVICE_ID, deviceId);
intent.putExtra(DeviceSettingsActivity.EXTRA_PLUGIN_KEY, pluginKey);
requireActivity().startActivity(intent);
}
});
}
public static class Builder extends AbstractBuilder<DeviceSettingsAlertDialogFragment.Builder, DeviceSettingsAlertDialogFragment> {
@Override
public DeviceSettingsAlertDialogFragment.Builder getThis() {
return this;
}
public DeviceSettingsAlertDialogFragment.Builder setPluginKey(String pluginKey) {
args.putString(KEY_PLUGIN_KEY, pluginKey);
return getThis();
}
public DeviceSettingsAlertDialogFragment.Builder setDeviceId(String deviceId) {
args.putString(KEY_DEVICE_ID, deviceId);
return getThis();
}
@Override
protected DeviceSettingsAlertDialogFragment createFragment() {
return new DeviceSettingsAlertDialogFragment();
}
}
}

View File

@@ -100,6 +100,7 @@ public class DeviceSettingsFragment extends PreferenceFragmentCompat {
List<String> plugins = device.getSupportedPlugins();
for (final String pluginKey : plugins) {
//TODO: Use PreferenceManagers context
PluginPreference pref = new PluginPreference(requireContext(), pluginKey, device, callback);
preferenceScreen.addPreference(pref);
}

View File

@@ -12,7 +12,7 @@ import androidx.annotation.NonNull;
import androidx.preference.CheckBoxPreference;
import androidx.preference.PreferenceViewHolder;
class PluginPreference extends CheckBoxPreference {
public class PluginPreference extends CheckBoxPreference {
private final Device device;
private final String pluginKey;
private final View.OnClickListener listener;

View File

@@ -78,4 +78,8 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
PluginFactory.PluginInfo info = PluginFactory.getPluginInfo(pluginKey);
requireActivity().setTitle(getString(R.string.plugin_settings_with_name, info.getDisplayName()));
}
public String getDeviceId() {
return ((DeviceSettingsActivity)requireActivity()).getDeviceId();
}
}

View File

@@ -22,7 +22,7 @@ import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreferenceCompat;
import androidx.preference.TwoStatePreference;
class SettingsFragment extends PreferenceFragmentCompat {
public class SettingsFragment extends PreferenceFragmentCompat {
private MainActivity mainActivity;
private EditTextPreference renameDevice;

View File

@@ -0,0 +1,76 @@
/*
* 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.async;
import java.util.concurrent.atomic.AtomicLong;
import androidx.annotation.NonNull;
public abstract class BackgroundJob<I, R> implements Runnable {
private static AtomicLong atomicLong = new AtomicLong(0);
protected volatile boolean canceled;
private BackgroundJobHandler backgroundJobHandler;
private long id;
protected I requestInfo;
private Callback<R> callback;
public BackgroundJob(I requestInfo, Callback<R> callback) {
this.id = atomicLong.incrementAndGet();
this.requestInfo = requestInfo;
this.callback = callback;
}
void setBackgroundJobHandler(BackgroundJobHandler handler) {
this.backgroundJobHandler = handler;
}
public long getId() { return id; }
public I getRequestInfo() { return requestInfo; }
public void cancel() {
canceled = true;
backgroundJobHandler.cancelJob(this);
}
public boolean isCancelled() {
return canceled;
}
public interface Callback<R> {
void onResult(@NonNull BackgroundJob job, R result);
void onError(@NonNull BackgroundJob job, @NonNull Throwable error);
}
protected void reportResult(R result) {
backgroundJobHandler.runOnUiThread(() -> {
callback.onResult(this, result);
backgroundJobHandler.onFinished(this);
});
}
protected void reportError(@NonNull Throwable error) {
backgroundJobHandler.runOnUiThread(() -> {
callback.onError(this, error);
backgroundJobHandler.onFinished(this);
});
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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.async;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import androidx.annotation.Nullable;
public class BackgroundJobHandler {
private static final String TAG = BackgroundJobHandler.class.getSimpleName();
private final Map<BackgroundJob, Future<?>> jobMap = new HashMap<>();
private final Object jobMapLock = new Object();
private class MyThreadPoolExecutor extends ThreadPoolExecutor {
MyThreadPoolExecutor(int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (!(r instanceof Future)) {
return;
}
Future<?> future = (Future<?>) r;
if (t == null) {
try {
future.get();
} catch (CancellationException ce) {
Log.d(TAG,"afterExecute got a CancellationException");
} catch (ExecutionException ee) {
t = ee;
} catch (InterruptedException ie) {
Log.d(TAG, "afterExecute got an InterruptedException");
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null) {
BackgroundJobHandler.this.handleUncaughtException(future, t);
}
}
}
private final ThreadPoolExecutor threadPoolExecutor;
private Handler handler;
private BackgroundJobHandler(int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this.handler = new Handler(Looper.getMainLooper());
this.threadPoolExecutor = new MyThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue);
}
public void runJob(BackgroundJob bgJob) {
Future<?> f;
bgJob.setBackgroundJobHandler(this);
try {
synchronized (jobMapLock) {
f = threadPoolExecutor.submit(bgJob);
jobMap.put(bgJob, f);
}
} catch (RejectedExecutionException e) {
Log.d(TAG,"threadPoolExecutor.submit rejected a background job: " + e.getMessage());
bgJob.reportError(e);
}
}
public boolean isRunning(long jobId) {
synchronized (jobMapLock) {
for (BackgroundJob job : jobMap.keySet()) {
if (job.getId() == jobId) {
return true;
}
}
}
return false;
}
@Nullable
public BackgroundJob getJob(long jobId) {
synchronized (jobMapLock) {
for (BackgroundJob job : jobMap.keySet()) {
if (job.getId() == jobId) {
return job;
}
}
}
return null;
}
void cancelJob(BackgroundJob job) {
synchronized (jobMapLock) {
if (jobMap.containsKey(job)) {
Future<?> f = jobMap.get(job);
if (f.cancel(true)) {
threadPoolExecutor.purge();
}
jobMap.remove(job);
}
}
}
private void handleUncaughtException(Future<?> ft, Throwable t) {
synchronized (jobMapLock) {
for (Map.Entry<BackgroundJob, Future<?>> pairs : jobMap.entrySet()) {
Future<?> future = pairs.getValue();
if (future == ft) {
pairs.getKey().reportError(t);
break;
}
}
}
}
void onFinished(BackgroundJob job) {
synchronized (jobMapLock) {
jobMap.remove(job);
}
}
void runOnUiThread(Runnable runnable) {
handler.post(runnable);
}
public static BackgroundJobHandler newFixedThreadPoolBackgroundJobHander(int numThreads) {
return new BackgroundJobHandler(numThreads, numThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
}
}