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

Compare commits

...

41 Commits

Author SHA1 Message Date
Albert Vaca Cintora
906c04ac1a Bump version number to release 2019-06-14 20:54:54 +02:00
Nicolas Fella
a61cb875f1 Only open file if open is actually true 2019-06-14 15:47:16 +02:00
Nicolas Fella
6e053a7e95 Hide keyboard display action if remote keyboard input is not supported 2019-06-13 19:28:45 +00:00
Nicolas Fella
71b034a025 Fix packet loss after connection 2019-06-13 17:33:21 +00:00
Matthijs Tijink
0e9dd25172 Enable the MPRIS server plugin - allows control of android media players
The changes allow loading the plugin on older Android versions
2019-06-10 15:59:37 +00:00
Simon Redman
51e957d822 [SMSApp] Support plain-text MMS
## Summary

Not having support for MMS caused some minor problems, like in https://bugs.kde.org/show_bug.cgi?id=398889 . This patch adds basic MMS support for plain-text MMS, including multi-target messages.

Android companion to https://invent.kde.org/kde/kdeconnect-kde/merge_requests/97

Currently there are several rough areas:
  - Multi-target messages do not have the full list of recipients (I am planning to work on this in another patch, because this one is already quite large enough)
  - Parsing MMS is significantly slower than parsing SMS. This makes sense, since we need to make significantly many more content:// calls for MMS. The only solution I can think of here is to add the ability to request a range of messages, which I need to do anyway, but which should not be part of this patch.
  - The desktop app is totally busted with regard to multi-target MMS, but that will also be fixed in another MR

BUG: 398889

## Test Plan

### Before:
Open SMS app on desktop, scroll through conversations, notice:
  - Any single-target message which had the most-recent message as an MMS does not appear
  - Any multi-target MMS conversations do not appear

### After:
Open SMS app on desktop, notice:
  - Conversations which have an MMS as their most-recent message appear
  - MMS which consisted of only text are rendered correctly
  - Multi-target conversations are shown (though pretty busted, as said before. Do not attempt to reply to one!)
2019-06-10 05:48:28 +00:00
l10n daemon script
ec43336153 GIT_SILENT made messages (after extraction) 2019-06-09 02:48:56 +02:00
Simon Redman
49295c0de9 Make lint suppression less aggressive for SMSHelper 2019-06-05 22:17:02 -06:00
Matthijs Tijink
56d01ed082 Fix pausing a mpris player when another is still playing 2019-06-04 13:12:51 +00:00
Nicolas Fella
dbd9ece110 refactor getTickerText 2019-06-04 13:01:34 +00:00
Nicolas Fella
132e4e7e0f Refactor extractRepliableNotification 2019-06-04 13:00:32 +00:00
Nicolas Fella
4cdda3f31b Refactor action extraction 2019-06-04 14:57:10 +02:00
Nicolas Fella
7c723eea8c Refactor icon extraction 2019-06-04 12:52:23 +00:00
Nicolas Fella
22e7b91bb3 Cleanup ReceiveNotification 2019-06-04 12:51:24 +00:00
Nicolas Fella
4aa365e4ff Cleanup upload notification 2019-06-04 12:50:35 +00:00
l10n daemon script
c50642e587 GIT_SILENT made messages (after extraction) 2019-06-02 02:48:49 +02:00
Matthijs Tijink
e52f418dad Properly close the media session when closing the media notification
BUG: 407812
2019-05-29 20:55:15 +00:00
l10n daemon script
6a7fbecc97 GIT_SILENT made messages (after extraction) 2019-05-26 02:47:22 +02:00
Nicolas Fella
db0c48cc6b Extract bigtext from notifications 2019-05-24 11:27:08 +00:00
Nicolas Fella
35635a0b0b remove unused parameter 2019-05-22 23:48:31 +02:00
Nicolas Fella
8a2cc4a841 remove unused ImagesHelper 2019-05-22 00:49:36 +02:00
Nicolas Fella
a53bf9b191 Simplify condition 2019-05-22 00:22:00 +02:00
Nicolas Fella
2d3b0e7641 Extract conversations from conversation notification style 2019-05-21 21:32:33 +00:00
l10n daemon script
274621e79d GIT_SILENT made messages (after extraction) 2019-05-21 02:47:16 +02:00
Albert Vaca Cintora
ea136498b4 Re-generate cert if it's for a different device ID
This would happen to people who transferred their KDE Connect config from
one phone to another (mostly with backup apps that only work on rooted
phones). This led to a state where other devices would always reject the
connection because the certificate CN didn't match the device ID.

On the PC side this is not a problem because the certificate is the source
of truth for the device ID.
2019-05-21 00:25:11 +02:00
Nicolas Fella
1e82c653d6 [DeviceFragment] Unify logging tags 2019-05-21 00:21:01 +02:00
Nicolas Fella
565be4a42a [plugins/notifications] Unify log tags 2019-05-21 00:06:15 +02:00
Nicolas Fella
de4e203d8c Fix typo 2019-05-20 22:25:26 +02:00
l10n daemon script
86e698df08 GIT_SILENT made messages (after extraction) 2019-05-20 02:49:40 +02:00
Nicolas Fella
3d5dcdacae Revert "Revert "Upgrade gradle plugin for AS 3.4""
This reverts commit 52486ed2ec.
2019-05-19 22:44:56 +02:00
Nicolas Fella
8b145b4c6a Simplify notification title/text extraction 2019-05-19 20:05:58 +02:00
Nicolas Fella
52486ed2ec Revert "Upgrade gradle plugin for AS 3.4"
This reverts commit 87ddf47999.

Seems to break running the app
2019-05-19 19:41:02 +02:00
Mitja Cotic
b8d327c2d9 [pretty please] allow fetching of cover art in mpris plugin from connected computer
Summary:
So from android 8 onwards it appears that fetching content from "cleartext" urls is disabled by default

https://stackoverflow.com/questions/45940861/android-8-cleartext-http-traffic-not-permitted

I have a mpris service running on my local computer which is connected to the android, that service is also serving cover art for currently playing song. Obviously I can not have a domain set for my computer on a local machine, so cover art urls look like
`http://<ip>:<port>/<songid>.ext` . Since this restriction was introduced into android 8, kde connect is not able to fetch the art from this url.

This is patch allows connections to raw IPs addresses as well. If there is any security issues regarding this change, I'm prepared to do more work, my suggestion would be to check if IP address in this case matches paired computer's IP address, although this would still be a bit annoying (especially since whatever malicious file can be served from a "cleartext" link as well).

Test Plan:
- start some sort of server which can serve an image on a paired computer
- create mpris Player instance on a paired computer and set the `mpris:artUrl` to the served address where this image is available
- open kde connect android app, select prepared player and see default cover art instead of the served image
- change artUrl to some image from the web
- check in kde connect again and see it correctly show a remote image

Reviewers: #kde_connect, nicolasfella, albertvaka

Reviewed By: #kde_connect, nicolasfella, albertvaka

Subscribers: albertvaka, nicolasfella, andyholmes, kdeconnect

Tags: #kde_connect

Differential Revision: https://phabricator.kde.org/D21247
2019-05-18 10:50:14 +02:00
l10n daemon script
630044e702 GIT_SILENT made messages (after extraction) 2019-05-16 02:51:33 +02:00
Nicolas Fella
720421554a Rework plugin list header 2019-05-13 22:34:10 +00:00
Nicolas Fella
e045964592 Declare permission for installing packages 2019-05-13 21:33:25 +00:00
Nicolas Fella
98931c7bcf Rename DeviceSettigsActivity->PluginSettingsActivity 2019-05-13 21:32:47 +00:00
Nicolas Fella
a616d5afbd Remove unused method 2019-05-13 21:31:58 +00:00
Nicolas Fella
1ffcaba71c Simplify code in createPluginList 2019-05-13 21:31:03 +00:00
l10n daemon script
260abb192c GIT_SILENT made messages (after extraction) 2019-05-06 03:07:52 +02:00
l10n daemon script
ea57aec40f GIT_SILENT made messages (after extraction) 2019-05-03 03:13:56 +02:00
32 changed files with 1389 additions and 481 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="11270"
android:versionName="1.12.7">
android:versionCode="11280"
android:versionName="1.12.8">
<supports-screens
android:anyDensity="true"
@@ -32,11 +32,13 @@
<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.REQUEST_INSTALL_PACKAGES" />
<application
android:icon="@drawable/icon"
android:label="KDE Connect"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/KdeConnectTheme">
<service
android:name="org.kde.kdeconnect.BackgroundService"
@@ -65,7 +67,7 @@
</intent-filter>
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.DeviceSettingsActivity"
android:name="org.kde.kdeconnect.UserInterface.PluginSettingsActivity"
android:label="@string/device_menu_plugins"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
@@ -259,10 +261,10 @@
<activity
android:name="org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationFilterActivity"
android:label="@string/title_activity_notification_filter"
android:parentActivityName="org.kde.kdeconnect.UserInterface.DeviceSettingsActivity">
android:parentActivityName="org.kde.kdeconnect.UserInterface.PluginSettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.DeviceSettingsActivity" />
android:value="org.kde.kdeconnect.UserInterface.PluginSettingsActivity" />
</activity>
<activity android:name="org.kde.kdeconnect.Plugins.PhotoPlugin.PhotoActivity" />

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingTop="28dp"
android:paddingRight="16dp"
android:paddingBottom="8dp" />

View File

@@ -1,64 +1,127 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="kde_connect">KDE Connect</string>
<string name="pref_plugin_telephony">Avisador telefónicu</string>
<string name="pref_plugin_battery">Informe de batería</string>
<string name="pref_plugin_battery_desc">Informe periódicu del estáu de la batería</string>
<string name="pref_plugin_sftp_desc">Permite restolar remotamente a esti preséu</string>
<string name="pref_plugin_telephony_desc">Unvia avisos de les llamaes entrantes</string>
<string name="pref_plugin_battery">Informe de la batería</string>
<string name="pref_plugin_battery_desc">Informa davezu del estáu de la batería</string>
<string name="pref_plugin_sftp_desc">Permite restolar remotamente\'l sistema de ficheros d\'esti preséu</string>
<string name="pref_plugin_clipboard">Sincronización del cartafueyu</string>
<string name="pref_plugin_clipboard_desc">Comparte\'l conteníu del cartafueyu</string>
<string name="pref_plugin_mousepad">Entrada remota</string>
<string name="pref_plugin_mousepad_desc">Usa\'l to teléfonu o tableta como panel táutil y tecláu</string>
<string name="pref_plugin_remotekeyboard">Pulsaciones remotes</string>
<string name="pref_plugin_mousepad_desc">Usa\'l preséu como panel táutil y tecláu</string>
<string name="pref_plugin_mpris">Controles multimedia</string>
<string name="pref_plugin_mpris_desc">Forne un control remotu pal to reproductor multimedia</string>
<string name="pref_plugin_runcommand">Execución de comandos</string>
<string name="pref_plugin_runcommand_desc">Aiciona comandos remotos del to teléfonu o tableta</string>
<string name="pref_plugin_ping">Ping</string>
<string name="pref_plugin_ping_desc">Unvia y recibe pings</string>
<string name="pref_plugin_notifications">Sincronización d\'avisos</string>
<string name="pref_plugin_notifications_desc">Accede a los tos avisos d\'otros preseos</string>
<string name="pref_plugin_receive_notifications">Recibir avisos</string>
<string name="pref_plugin_receive_notifications_desc">Recibe avisos d\'otros preseos y amuésalos n\'Android</string>
<string name="pref_plugin_sharereceiver">Compartir y recibir</string>
<string name="pref_plugin_sharereceiver_desc">Comparte ficheros y URLs ente preseos</string>
<string name="plugin_not_available">Esta carauterística nun ta disponible na to versión d\'Android</string>
<string name="device_list_empty">Ensin preseos</string>
<string name="ok">Aceutar</string>
<string name="pref_plugin_runcommand_desc">Aiciona comandos remotos dende\'l preséu</string>
<string name="pref_plugin_contacts">Sincronizador de contautos</string>
<string name="pref_plugin_notifications_desc">Accede a los avisos n\'otros preseos</string>
<string name="cancel">Encaboxar</string>
<string name="open_settings">Abrir axustes</string>
<string name="no_permissions">¡</string>
<string name="send_ping">Unviar ping</string>
<string name="open_mpris_controls">Control multimedia</string>
<string name="open_mousepad">Entrada remota</string>
<string-array name="mousepad_tap_entries">
<item>Right click</item>
<item>Middle click</item>
<item>Nothing</item>
<item>Clic drechu</item>
<item>Clic d\'en mediu</item>
<item>Nada</item>
</string-array>
<string-array name="mousepad_sensitivity_entries">
<item>Slowest</item>
<item>Above Slowest</item>
<item>Perlenta</item>
<item>Lenta</item>
<item>Default</item>
<item>Above Default</item>
<item>Fastest</item>
<item>Rápida</item>
<item>Perrápida</item>
</string-array>
<string-array name="mousepad_acceleration_profile_entries">
<item>No Acceleration</item>
<item>Ensin aceleración</item>
<item>Weakest</item>
<item>Weaker</item>
<item>Medium</item>
<item>Stronger</item>
<item>Strongest</item>
<item>Normal</item>
<item>Fuerte</item>
<item>Perfuerte</item>
</string-array>
<string name="error_timed_out">Escosó\'l tiempu</string>
<string name="category_connected_devices">Preseos coneutaos</string>
<string name="category_not_paired_devices">Preseos disponibles</string>
<string name="category_remembered_devices">Preseos recordaos</string>
<string name="device_menu_unpair">Desempareyar</string>
<string name="pair_new_device">Empareya un preséu nuevu</string>
<plurals name="incoming_file_title">
<item quantity="one">Recibiendo %1$d ficheru de %2$s</item>
<item quantity="other">Recibiendo %1$d ficheros de %2$s</item>
</plurals>
<plurals name="incoming_files_text">
<item quantity="one">Ficheru: %1s</item>
<item quantity="other">(Ficheru %2$d de %3$d): %1$s</item>
</plurals>
<plurals name="outgoing_file_title">
<item quantity="one">Unviando %1$d ficheru a %2$s</item>
<item quantity="other">Unviando %1$d ficheros a %2$s</item>
</plurals>
<plurals name="outgoing_files_text">
<item quantity="one">Ficheru: %1$s</item>
<item quantity="other">(Ficheru %2$d de %3$d): %1$s</item>
</plurals>
<plurals name="received_files_fail_title">
<item quantity="one">Fallu al recibir el ficheru de %1$s</item>
<item quantity="other">Fallu al recibir %2$d de los %3$d ficheros de %1$s</item>
</plurals>
<plurals name="send_files_fail_title">
<item quantity="one">Fallu al unviar el ficheru de %1$s</item>
<item quantity="other">Fallu al unviar %2$d de los %3$d ficheros de %1$s</item>
</plurals>
<string name="cannot_create_file">Nun pue crease\'l ficheru %s</string>
<string name="right_click">Unviar un clic drechu</string>
<string name="middle_click">Unviar un clic d\'en mediu</string>
<string name="show_keyboard">Amosar el tecláu</string>
<string name="device_not_paired">Nun s\'empareyó\'l preséu</string>
<string name="request_pairing">Solicitar l\'empareyamientu</string>
<string name="device">Preséu</string>
<string name="mpris_volume">Volume</string>
<string-array name="mpris_time_entries">
<item>10 seconds</item>
<item>20 seconds</item>
<item>30 seconds</item>
<item>1 minute</item>
<item>2 minutes</item>
<item>10 segundos</item>
<item>20 segundos</item>
<item>30 segundos</item>
<item>1 minutu</item>
<item>2 minutos</item>
</string-array>
<string name="no_file_browser">Nun hai restoladores de ficheros instalaos</string>
<string name="pref_plugin_telepathy_desc">Unvia mensaxes de testu dende\'l to escritoriu</string>
<string name="plugin_not_supported">Esti complementu nun lu sofita\'l preséu</string>
<string name="findmyphone_description">Fai sonar el teléfonu pa qu\'asina pueas alcontralu</string>
<string name="protocol_version_older">Esti preséu una una versión vieya del protocolu</string>
<string name="protocol_version_newer">Esti preséu una una versión nueva del protocolu</string>
<string name="general_settings">Axustes xenerales</string>
<string name="plugin_settings">Axustes</string>
<string name="device_name_preference_summary">%s</string>
<string name="custom_devices_settings">Llista de preseos personalizaos</string>
<string name="custom_device_list">Amestar preseos pola IP</string>
<string name="share_notification_preference">Avisos sonoros</string>
<string name="share_notification_preference_summary">Vibra y reproduz un soníu al recibir un ficheru</string>
<string name="share_destination_customize_summary_disabled">Los ficheros recibíos van apaecer en Descargues</string>
<string name="share_destination_folder_preference">Direutoriu de destín</string>
<string name="title_activity_notification_filter">Peñera d\'avisos</string>
<string name="filter_apps_info">Van sincronizase los avisos de les aplicaciones esbillaes.</string>
<string name="sftp_sdcard">Tarxeta SD</string>
<string name="add_device_dialog_title">Amiestu d\'un preséu</string>
<string name="add_device_hint">Nome d\'agopiu o direición IP</string>
<string name="no_players_connected">Nun s\'alcontraron reproductores</string>
<string name="send_files">Unviar ficheros</string>
<string name="pairing_description">Equí deberíen apaecer los demás preseos que tean na mesma rede y executando KDE Connect.</string>
<string name="device_rename_title">Renomáu del preséu</string>
<string name="device_rename_confirm">Renomar</string>
<string name="refresh">Refrescar</string>
<string name="pref_plugin_telepathy_desc">Unvia SMS dende l\'ordenador</string>
<string name="findmyphone_description">Fai qu\'esti preséu suene pa que pueas alcontralu</string>
<string name="telephony_pref_blocked_title">Númberos bloquiaos</string>
<string name="presenter_fullscreen">Pantalla completa</string>
<string name="presenter_exit">Colar de la presentación</string>
<string name="addcommand_explanation">Nun hai comandos rexistraos.</string>
<string name="add_command_description">Pues amestar más comandos nel escritoriu</string>
<string name="dark_theme">Estilu escuru</string>
<string name="notification_channel_persistent">Indicador permanente</string>
<string name="clipboard_toast">Copióse al cartafueyu</string>
<string name="pref_plugin_systemvolume">Volume del sistema</string>
<string name="pref_plugin_systemvolume_desc">Controla\'l volume del sistema del preséu remotu</string>
<string name="devices">Preseos</string>
<string name="settings_dark_mode">Estilu escuru</string>
<string name="settings_more_settings_title">Más axustes</string>
<string name="setting_persistent_notification_oreo">Avisu permanente</string>
<string name="extra_options">Opciones adicionales</string>
<string name="new_notification">Avisu nuevu</string>
<string name="notification_channel_receivenotification">Avisos d\'otros preseos</string>
<string name="plugin_photo_desc">Llanza l\'aplicación de la cámarra p\'acenciellar la fechura y tresferencia de semeyes</string>
</resources>

View File

@@ -104,6 +104,14 @@
<item quantity="one">File: %1s</item>
<item quantity="other">(File %2$d di %3$d) : %1$s</item>
</plurals>
<plurals name="outgoing_file_title">
<item quantity="one">Invio di %1$d file a %2$s</item>
<item quantity="other">Invio di %1$d file a %2$s</item>
</plurals>
<plurals name="outgoing_files_text">
<item quantity="one">File: %1$s</item>
<item quantity="other">(File %2$d di %3$d) : %1$s</item>
</plurals>
<plurals name="received_files_title">
<item quantity="one">File ricevuto da %1$s</item>
<item quantity="other">Ricevuti %2$d file da %1$s</item>
@@ -112,6 +120,14 @@
<item quantity="one">Ricezione file da di %1$s non riuscita</item>
<item quantity="other">Ricezione di %2$d di %3$d file da %1$s non riuscita</item>
</plurals>
<plurals name="sent_files_title">
<item quantity="one">File inviato a %1$s</item>
<item quantity="other">Inviati %2$d file a %1$s</item>
</plurals>
<plurals name="send_files_fail_title">
<item quantity="one">Invio del file a %1$s non riuscito</item>
<item quantity="other">Invio di %2$d di %3$d file a %1$s non riuscito</item>
</plurals>
<string name="received_file_text">Tocca per aprire «%1s»</string>
<string name="cannot_create_file">Impossibile creare il file %s</string>
<string name="tap_to_answer">Tocca per rispondere</string>
@@ -230,6 +246,7 @@
<string name="plugins_need_optional_permission">Alcune estensioni hanno funzioni disabilitate per una mancanza di permessi (tocca per maggiori informazioni):</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 dal desktop devi dare l\'autorizzazione per accedere al registro delle chiamate e allo stato del telefono</string>
<string name="telephony_optional_permission_explanation">Per vedere il nome di un contatto invece del numero di telefono devi dare accesso alla rubrica del telefono</string>
<string name="contacts_permission_explanation">Per condividere la tua rubrica con il desktop, devi concedere l\'autorizzazione per i contatti</string>
<string name="select_ringtone">Seleziona una suoneria</string>

View File

@@ -1,30 +1,259 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="foreground_notification_no_devices">デバイスに接続されていません</string>
<string name="foreground_notification_devices">接続済み: %s</string>
<string name="pref_plugin_telephony">電話通知</string>
<string name="pref_plugin_telephony_desc">着信通知を送信</string>
<string name="pref_plugin_battery">バッテリーレポート</string>
<string name="pref_plugin_battery_desc">定期的にバッテリー状態を報告します</string>
<string name="pref_plugin_sftp">ファイルシステムの参照</string>
<string name="pref_plugin_sftp_desc">リモートからこのデバイスのファイルシステムへの閲覧を可能にします</string>
<string name="pref_plugin_clipboard">クリップボードの同期</string>
<string name="pref_plugin_clipboard_desc">クリップボードの内容を共有</string>
<string name="pref_plugin_mousepad">リモート入力</string>
<string name="pref_plugin_mousepad_desc">あなたのスマートフォンやタブレットをタッチパッドやキーボードとして利用します</string>
<string name="pref_plugin_presenter">リモートスライドショー</string>
<string name="pref_plugin_presenter_desc">あなたのデバイスを使ってプレゼンテーションのスライドを変更</string>
<string name="pref_plugin_remotekeyboard">リモートからキー入力を受信</string>
<string name="pref_plugin_mpris">マルチメディアの操作</string>
<string name="pref_plugin_mpris_desc">あなたのメディアプレーヤーへのリモート操作を提供します</string>
<string name="pref_plugin_runcommand">コマンドを実行</string>
<string name="pref_plugin_runcommand_desc">スマートフォンやタブレットからリモートコマンドを実行</string>
<string name="pref_plugin_contacts_desc">デバイスの連絡先の同期を許可</string>
<string name="pref_plugin_ping_desc">Ping を送受信</string>
<string name="pref_plugin_notifications">通知の同期</string>
<string name="pref_plugin_notifications_desc">他のデバイスから通知にアクセス</string>
<string name="pref_plugin_receive_notifications">通知の受信</string>
<string name="pref_plugin_receive_notifications_desc">他のデバイスから通知を受信し、Android に表示します</string>
<string name="pref_plugin_sharereceiver">共有と受信</string>
<string name="pref_plugin_sharereceiver_desc">デバイス間でファイルと URL を共有</string>
<string name="plugin_not_available">この機能はあなたの Android バージョンでは利用できません</string>
<string name="device_list_empty">デバイスなし</string>
<string name="cancel">キャンセル</string>
<string name="open_settings">設定を開く</string>
<string name="no_permissions">通知にアクセスするには権限を許可する必要があります</string>
<string name="no_permissions_remotekeyboard">キー入力を受信するには KDE Connect リモートキーボードをアクティブ化する必要があります</string>
<string name="send_ping">Ping を送信</string>
<string name="open_mpris_controls">マルチメディアの操作</string>
<string name="remotekeyboard_connected">リモートキーボード接続はアクティブです</string>
<string name="open_mousepad">リモート入力</string>
<string name="mousepad_info">スクリーン上で指を動かしてマウスカーソルを移動します。タップはクリックとなり、2/3本指で右クリック、中クリックとなります。2本指を使ってスクロールが可能です。長押しすることでドラッグ&amp;ドロップできます。</string>
<string name="mousepad_double_tap_settings_title">2本指タップのアクションを設定</string>
<string name="mousepad_triple_tap_settings_title">3本指タップのアクションを設定</string>
<string name="mousepad_sensitivity_settings_title">タッチパッドの感度を設定</string>
<string name="mousepad_acceleration_profile_settings_title">ポインタの速度を設定</string>
<string name="mousepad_scroll_direction_title">スクロールの方向を反転</string>
<string-array name="mousepad_tap_entries">
<item>Right click</item>
<item>Middle click</item>
<item>Nothing</item>
<item>右クリック</item>
<item>中クリック</item>
<item>なし</item>
</string-array>
<string-array name="mousepad_sensitivity_entries">
<item>Slowest</item>
<item>Above Slowest</item>
<item>Default</item>
<item>デフォルト</item>
<item>Above Default</item>
<item>Fastest</item>
</string-array>
<string-array name="mousepad_acceleration_profile_entries">
<item>No Acceleration</item>
<item>Weakest</item>
<item>Weaker</item>
<item>Medium</item>
<item>Stronger</item>
<item>Strongest</item>
<item>加速なし</item>
<item>最弱</item>
<item></item>
<item></item>
<item></item>
<item>最強</item>
</string-array>
<string name="category_connected_devices">接続済みのデバイス</string>
<string name="category_not_paired_devices">利用可能なデバイス</string>
<string name="device_menu_plugins">プラグイン設定</string>
<string name="device_menu_unpair">ペアリング解除</string>
<string name="device_not_reachable">ペアリング済みのデバイスに到達できません</string>
<string name="pair_new_device">新しいデバイスをペアリング</string>
<string name="unknown_device">不明なデバイス</string>
<string name="error_not_reachable">デバイスは到達不可です</string>
<string name="error_already_requested">ペアリングは既に要求済みです</string>
<string name="error_already_paired">デバイスは既にペアリング済みです</string>
<string name="error_timed_out">タイムアウト</string>
<string name="error_canceled_by_user">ユーザにキャンセルされました</string>
<string name="error_canceled_by_other_peer">他のピアにキャンセルされました</string>
<string name="error_invalid_key">不正なキーを受信しました</string>
<string name="encryption_info_title">暗号化情報</string>
<string name="encryption_info_msg_no_ssl">他のデバイスは最近のバージョンの KDE Connect を利用していません。古い暗号化方式を使用しています</string>
<string name="pair_requested">ペアリング要求済み</string>
<string name="pairing_request_from">%1s からペアリングを要求されました</string>
<string name="received_url_title">%1s からリンクを受信</string>
<string name="received_url_text">タップして \'%1s\' を開く</string>
<plurals name="incoming_file_title">
<item quantity="other">%1$d ファイルを %2$s から受信しています</item>
</plurals>
<plurals name="incoming_files_text">
<item quantity="other">ファイル: %1s</item>
</plurals>
<plurals name="outgoing_file_title">
<item quantity="other">%1$d ファイルを %2$s へ送信中</item>
</plurals>
<plurals name="outgoing_files_text">
<item quantity="other">ファイル: %1$s</item>
</plurals>
<plurals name="received_files_title">
<item quantity="other">%1$s からファイルを受信</item>
</plurals>
<plurals name="received_files_fail_title">
<item quantity="other">%1$s からのファイルの受信に失敗</item>
</plurals>
<plurals name="sent_files_title">
<item quantity="other">%1$s にファイルを送信済み</item>
</plurals>
<string name="received_file_text">タップして \'%1s\' を開く</string>
<string name="tap_to_answer">タップして応答</string>
<string name="reconnect">再接続</string>
<string name="right_click">右クリックを送信</string>
<string name="middle_click">中クリックを送信</string>
<string name="show_keyboard">キーボードを表示</string>
<string name="device_not_paired">デバイスはペアリングされていません</string>
<string name="request_pairing">ペアリングを要求</string>
<string name="pairing_accept">許可</string>
<string name="pairing_reject">拒否</string>
<string name="device">デバイス</string>
<string name="pair_device">デバイスをペアリング</string>
<string name="settings">設定</string>
<string name="mpris_play">再生</string>
<string name="mpris_pause">一時停止</string>
<string name="mpris_previous">前へ</string>
<string name="mpris_rew">巻き戻し</string>
<string name="mpris_next">次へ</string>
<string name="mpris_volume">音量</string>
<string name="mpris_settings">マルチメディアの設定</string>
<string name="mpris_time_settings_title">早送り/巻き戻しボタン</string>
<string name="mpris_time_settings_summary">押した時に早送り/巻き戻しする時間を調整</string>
<string-array name="mpris_time_entries">
<item>10 seconds</item>
<item>20 seconds</item>
<item>30 seconds</item>
<item>1 minute</item>
<item>2 minutes</item>
<item>10 </item>
<item>20 </item>
<item>30 </item>
<item>1 </item>
<item>2 </item>
</string-array>
<string name="mpris_notification_settings_title">メディア操作の通知を表示</string>
<string name="mpris_notification_settings_summary">KDE Connect を開かずにメディアプレーヤーをコントロール可能にします</string>
<string name="share_to">共有先...</string>
<string name="protocol_version_older">このデバイスは古いプロトコルバージョンを使用しています</string>
<string name="protocol_version_newer">このデバイスはより新しいプロトコルバージョンを使用しています</string>
<string name="general_settings">全般設定</string>
<string name="plugin_settings">設定</string>
<string name="plugin_settings_with_name">%s 設定</string>
<string name="device_name">デバイス名</string>
<string name="invalid_device_name">不正なデバイス名</string>
<string name="pair_device_action">新しいデバイスをペアリング</string>
<string name="unpair_device_action">%s をペアリング解除</string>
<string name="custom_device_list">IP アドレスでデバイスを追加</string>
<string name="delete_custom_device">%s を削除しますか?</string>
<string name="custom_device_fab_hint">デバイスを追加</string>
<string name="undo">元に戻す</string>
<string name="share_notification_preference">うるさい通知</string>
<string name="share_notification_preference_summary">ファイル受信時にバイブレートし、音声を再生します</string>
<string name="share_destination_customize">行き先ディレクトリをカスタマイズ</string>
<string name="share_destination_customize_summary_disabled">受信したファイルは Downloads に保存されます</string>
<string name="share_destination_customize_summary_enabled">ファイルは以下のディレクトリに保存されます</string>
<string name="share_destination_folder_preference">行き先ディレクトリ</string>
<string name="share">共有</string>
<string name="share_received_file">\"%s\" を共有</string>
<string name="title_activity_notification_filter">通知フィルタ</string>
<string name="filter_apps_info">選択されたアプリケーションの通知が同期されます。</string>
<string name="sftp_internal_storage">内部ストレージ</string>
<string name="sftp_sdcard_num">SD カード %d</string>
<string name="sftp_sdcard">SD カード</string>
<string name="sftp_readonly">(読み取り専用)</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_configured_storage_locations">設定されたストレージの場所</string>
<string name="sftp_preference_add_storage_location_title">ストレージの場所を追加</string>
<string name="sftp_preference_edit_storage_location">ストレージの場所を編集</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_hint">ホスト名かIPアドレス</string>
<string name="no_players_connected">プレーヤーが見つかりませんでした</string>
<string name="send_files">ファイルを送信</string>
<string name="pairing_title">KDE Connect デバイス</string>
<string name="pairing_description">同じネットワーク内で KDE Connect を実行しているデバイスがここに表示されます</string>
<string name="device_paired">デバイス ペアリング済み</string>
<string name="device_rename_title">デバイス名を変更</string>
<string name="device_rename_confirm">名前変更</string>
<string name="refresh">更新</string>
<string name="unreachable_description">このペアリング済みデバイスは到達不可です。同一のネットワーク内に 接続されていることを確認してください</string>
<string name="on_data_message">モバイルデータ接続を使用しているようです。KDE Connect はローカルネットワークでしか機能しません</string>
<string name="no_file_browser">ファイルブラウザがインストールされていません</string>
<string name="pref_plugin_telepathy">SMS を送信</string>
<string name="pref_plugin_telepathy_desc">デスクトップからテキストメッセージを送信</string>
<string name="plugin_not_supported">このプラグインはデバイスにサポートされていません</string>
<string name="findmyphone_title">スマートフォンを捜索</string>
<string name="findmyphone_title_tablet">タブレットを捜索</string>
<string name="findmyphone_title_tv">TV を捜索</string>
<string name="findmyphone_description">このデバイスを鳴らすことで捜索できます</string>
<string name="findmyphone_found">発見</string>
<string name="open">開く</string>
<string name="close">閉じる</string>
<string name="no_permissions_storage">ストレージにアクセスするには権限を許可する必要があります</string>
<string name="plugins_need_permission">いくつかのプラグインが機能するには権限が必要です (タップして詳細情報を表示):</string>
<string name="permission_explanation">このプラグインが機能するには権限が必要です</string>
<string name="optional_permission_explanation">すべての機能を有効にするには、追加の権限を許可する必要があります</string>
<string name="plugins_need_optional_permission">権限が不足しているため、いくつかのプラグインの機能は無効化されています (タップして詳細情報を表示):</string>
<string name="share_optional_permission_explanation">スマートフォンとデスクトップ間でファイルを共有するには、端末のストレージへのアクセスを許可する必要があります</string>
<string name="telepathy_permission_explanation">デスクトップから SMS を送受信するには SMS の権限を付与する必要があります</string>
<string name="telephony_optional_permission_explanation">電話番号ではなく連絡先名を見るには、端末の連絡先へのアクセスを許可する必要があります</string>
<string name="contacts_permission_explanation">連絡先をデスクトップと共有するために、連絡先の権限が必要です</string>
<string name="select_ringtone">着信音を選択</string>
<string name="telephony_pref_blocked_title">ブロックされた番号</string>
<string name="telephony_pref_blocked_dialog_desc">これらの番号からの通話と SMS を表示しません。一行に1つの電話番号を指定してください</string>
<string name="mpris_coverart_description">現在のメディアのカバーアート</string>
<string name="device_icon_description">デバイスアイコン</string>
<string name="settings_icon_description">設定アイコン</string>
<string name="presenter_fullscreen">フルスクリーン</string>
<string name="presenter_exit">プレゼンテーションを終了</string>
<string name="presenter_lock_tip">デバイスをロックして、音量キーでスライドを前/次に移動できます</string>
<string name="add_command">コマンドを追加</string>
<string name="addcommand_explanation">コマンドが登録されていません</string>
<string name="addcommand_explanation2">KDE Connect のシステム設定で新しいコマンドを追加できます</string>
<string name="add_command_description">デスクトップから新しいコマンドを追加できます</string>
<string name="pref_plugin_mprisreceiver">メディアプレーヤーの操作</string>
<string name="pref_plugin_mprisreceiver_desc">他のデバイスからスマートフォンのメディアプレーヤーを操作</string>
<string name="dark_theme">ダーク テーマ</string>
<string name="notification_channel_default">他の通知</string>
<string name="notification_channel_persistent">永続的なインジケータ</string>
<string name="notification_channel_media_control">メディアの操作</string>
<string name="notification_channel_filetransfer">ファイル転送</string>
<string name="mpris_stop">現在のプレーヤーを停止</string>
<string name="copy_url_to_clipboard">URL をクリップボードにコピー</string>
<string name="clipboard_toast">クリップボードにコピーしました</string>
<string name="runcommand_notreachable">デバイスは到達不可です</string>
<string name="runcommand_notpaired">デバイスはペアリングされていません</string>
<string name="runcommand_noruncommandplugin">このデバイスは \'コマンドを実行\' プラグインを有効にしていません</string>
<string name="pref_plugin_findremotedevice">リモートデバイスを捜索</string>
<string name="pref_plugin_findremotedevice_desc">リモートデバイスを捜索</string>
<string name="pref_plugin_systemvolume">システム音量</string>
<string name="pref_plugin_systemvolume_desc">リモートデバイスのシステム音量を操作</string>
<string name="mute">ミュート</string>
<string name="all">すべて</string>
<string name="devices">デバイス</string>
<string name="settings_rename">デバイス名</string>
<string name="settings_dark_mode">ダーク テーマ</string>
<string name="settings_more_settings_title">その他の設定</string>
<string name="settings_more_settings_text">デバイスごとの設定はデバイス内の \'プラグイン設定\' にあります</string>
<string name="setting_persistent_notification">永続的な通知を表示</string>
<string name="setting_persistent_notification_oreo">永続的な通知</string>
<string name="setting_persistent_notification_description">タップして通知設定内で有効化/無効化します</string>
<string name="extra_options">追加オプション</string>
<string name="privacy_options">プライバシーオプション</string>
<string name="set_privacy_options">プライバシーオプションを設定</string>
<string name="new_notification">新しい通知</string>
<string name="notification_channel_receivenotification">他のデバイスからの通知</string>
<string name="take_picture">カメラを起動</string>
</resources>

View File

@@ -1,6 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="kde_connect">KDE Connect</string>
<string name="foreground_notification_no_devices">연결된 장치 없음</string>
<string name="foreground_notification_devices">연결됨: %s</string>
<string name="pref_plugin_telephony">전화 알림이</string>
<string name="pref_plugin_telephony_desc">수신 통화 알림 보내기</string>
<string name="pref_plugin_battery">배터리 보고</string>
<string name="pref_plugin_battery_desc">주기적으로 배터리 상태 보고</string>
<string name="pref_plugin_sftp">파일 시스템 보기</string>
@@ -9,6 +13,7 @@
<string name="pref_plugin_clipboard_desc">클립보드 내용 동기화</string>
<string name="pref_plugin_mousepad">원격 입력</string>
<string name="pref_plugin_mousepad_desc">내 휴대폰이나 태블릿을 터치패드와 키보드로 사용하기</string>
<string name="pref_plugin_presenter">슬라이드 쇼 리모콘</string>
<string name="pref_plugin_presenter_desc">내 장치로 프레젠테이션 슬라이드 전환하기</string>
<string name="pref_plugin_remotekeyboard">원격 키 입력 받기</string>
<string name="pref_plugin_remotekeyboard_desc">원격 장치의 키 입력 이벤트 받기</string>
@@ -33,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>
@@ -90,7 +96,32 @@
<string name="pairing_request_from">%1s에서 연결 요청</string>
<string name="received_url_title">%1s에서 링크 받음</string>
<string name="received_url_text">\'%1s\'을(를) 열려면 누르십시오</string>
<plurals name="incoming_file_title">
<item quantity="other">%2$s에서 보낸 파일 %1$d개 받음</item>
</plurals>
<plurals name="incoming_files_text">
<item quantity="other">(파일 %3$d개 중 %2$d개): %1$s</item>
</plurals>
<plurals name="outgoing_file_title">
<item quantity="other">%2$s(으)로 파일 %1$d개 보내는 중</item>
</plurals>
<plurals name="outgoing_files_text">
<item quantity="other">(파일 %3$d개 중 %2$d개): %1$s</item>
</plurals>
<plurals name="received_files_title">
<item quantity="other">%1$s에서 파일 %2$d개 받음</item>
</plurals>
<plurals name="received_files_fail_title">
<item quantity="other">%1$s에서 보낸 파일 %3$d개 중 %2$d개를 받을 수 없음</item>
</plurals>
<plurals name="sent_files_title">
<item quantity="other">%1$s(으)로 파일 %2$d개 보냄</item>
</plurals>
<plurals name="send_files_fail_title">
<item quantity="other">%1$s(으)로 파일 %3$d개 중 %2$d개를 보낼 수 없음</item>
</plurals>
<string name="received_file_text">\'%1s\'을(를) 열려면 누르십시오</string>
<string name="cannot_create_file">파일 %s을(를) 만들 수 없음</string>
<string name="tap_to_answer">눌러서 응답하기</string>
<string name="reconnect">다시 연결</string>
<string name="right_click">오른쪽 단추 누름 신호 보내기</string>
@@ -102,6 +133,7 @@
<string name="pairing_reject">거부</string>
<string name="device">장치</string>
<string name="pair_device">장치 연결</string>
<string name="settings">설정</string>
<string name="mpris_play">재생</string>
<string name="mpris_pause">일시 정지</string>
<string name="mpris_previous">이전</string>
@@ -111,6 +143,7 @@
<string name="mpris_volume">음량</string>
<string name="mpris_settings">멀티미디어 설정</string>
<string name="mpris_time_settings_title">되감기/빨리감기 단추</string>
<string name="mpris_time_settings_summary">눌렀을 때 되감거나 빨리 감을 시간 조정</string>
<string-array name="mpris_time_entries">
<item>10초</item>
<item>20초</item>
@@ -119,6 +152,8 @@
<item>2분</item>
</string-array>
<string name="mpris_notification_settings_title">미디어 제어 알림 보이기</string>
<string name="mpris_notification_settings_summary">KDE Connect를 열지 않고 미디어 재생기 제어</string>
<string name="share_to">다음으로 공유…</string>
<string name="protocol_version_older">이 장치의 프로토콜 버전이 오래되었습니다</string>
<string name="protocol_version_newer">이 장치의 프로토콜 버전이 더 새롭습니다</string>
<string name="general_settings">일반 설정</string>
@@ -127,11 +162,16 @@
<string name="device_name">장치 이름</string>
<string name="device_name_preference_summary">%s</string>
<string name="invalid_device_name">잘못된 장치 이름</string>
<string name="shareplugin_text_saved">텍스트 수신, 클립보드 복사됨</string>
<string name="shareplugin_text_saved">텍스트 수신, 클립보드 복사됨</string>
<string name="custom_devices_settings">사용자 정의 장치 목록</string>
<string name="pair_device_action">새 장치 연결</string>
<string name="unpair_device_action">%s 연결 해제</string>
<string name="custom_device_list">IP로 장치 추가</string>
<string name="delete_custom_device">%s을(를) 삭제하시겠습니까?</string>
<string name="custom_device_deleted">사용자 정의 장치 삭제됨</string>
<string name="custom_device_list_help">장치가 자동으로 감지되지 않았다면 떠 다니는 동작 단추를 눌러서 IP 주소나 호스트 이름으로 장치를 수동으로 추가할 수 있습니다</string>
<string name="custom_device_fab_hint">장치 추가</string>
<string name="undo">실행 취소</string>
<string name="share_notification_preference">시끄러운 알림</string>
<string name="share_notification_preference_summary">파일을 받았을 때 진동과 소리로 알림</string>
<string name="share_destination_customize">대상 디렉터리 사용자 정의</string>
@@ -147,7 +187,28 @@
<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">%2$s의 %1$s</string>
<string name="send_files">파일 보내기</string>
@@ -177,6 +238,7 @@
<string name="plugins_need_optional_permission">일부 플러그인은 권한이 없어서 비활성화되었습니다(정보를 보려면 누르기):</string>
<string name="share_optional_permission_explanation">휴대폰과 데스크톱간 파일을 공유하려면 휴대폰 저장소 접근 권한이 필요합니다</string>
<string name="telepathy_permission_explanation">데스크톱에서 문자 메시지를 읽고 보내려면 문자 메시지 접근 권한이 필요합니다</string>
<string name="telephony_permission_explanation">데스크톱에서 통화와 문자 메시지를 보려면 통화 기록 및 휴대폰 상태 접근 권한이 필요합니다</string>
<string name="telephony_optional_permission_explanation">전화번호 대신 연락처에 등록된 이름을 보려면 주소록 접근 권한이 필요합니다</string>
<string name="contacts_permission_explanation">데스크톱과 주소록을 공유하려면 주소록 접근 권한이 필요합니다</string>
<string name="select_ringtone">벨소리 선택</string>
@@ -187,6 +249,7 @@
<string name="settings_icon_description">설정 아이콘</string>
<string name="presenter_fullscreen">전체 화면</string>
<string name="presenter_exit">프레젠테이션 끝내기</string>
<string name="presenter_lock_tip">장치 잠금 상태에서는 음량 키를 이전/다음 슬라이드 단추로 사용할 수 있습니다</string>
<string name="add_command">명령 추가</string>
<string name="addcommand_explanation">등록된 명령이 없습니다</string>
<string name="addcommand_explanation2">시스템 설정의 KDE Connect에서 새로운 명령을 추가할 수 있습니다</string>
@@ -194,9 +257,13 @@
<string name="pref_plugin_mprisreceiver">미디어 재생기 제어</string>
<string name="pref_plugin_mprisreceiver_desc">다른 장치에서 휴대폰 미디어 재생기 제어</string>
<string name="dark_theme">어두운 테마</string>
<string name="mpris_stop">현재 플레이어 정지</string>
<string name="notification_channel_default">기타 알림</string>
<string name="notification_channel_persistent">항상 표시된 표시기</string>
<string name="notification_channel_media_control">미디어 제어</string>
<string name="notification_channel_filetransfer">파일 전송</string>
<string name="mpris_stop">현재 재생기 정지</string>
<string name="copy_url_to_clipboard">클립보드로 URL 복사</string>
<string name="clipboard_toast">클립보드 복사됨</string>
<string name="clipboard_toast">클립보드 복사됨</string>
<string name="runcommand_notreachable">장치에 접근할 수 없음</string>
<string name="runcommand_notpaired">장치가 연결되지 않음</string>
<string name="runcommand_nosuchdevice">장치가 없음</string>
@@ -207,4 +274,23 @@
<string name="pref_plugin_systemvolume">시스템 음량</string>
<string name="pref_plugin_systemvolume_desc">원격 장치의 시스템 음량 제어</string>
<string name="mute">음소거</string>
<string name="all">모두</string>
<string name="devices">장치</string>
<string name="settings_rename">장치 이름</string>
<string name="settings_dark_mode">어두운 테마</string>
<string name="settings_more_settings_title">더 많은 설정</string>
<string name="settings_more_settings_text">장치별 설정은 각각 장치의 \'플러그인 설정\'에서 확인할 수 있습니다.</string>
<string name="setting_persistent_notification">항상 표시되는 알림 보이기</string>
<string name="setting_persistent_notification_oreo">항상 표시되는 알림</string>
<string name="setting_persistent_notification_description">알림 설정에서 활성화/비활성화하려면 누르십시오</string>
<string name="extra_options">추가 설정</string>
<string name="privacy_options">프라이버시 설정</string>
<string name="set_privacy_options">프라이버시 설정 변경</string>
<string name="new_notification">새 알림</string>
<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="no_app_for_opening">이 파일을 열 수 있는 앱을 찾을 수 없음</string>
</resources>

View File

@@ -1,6 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="kde_connect">KDE Connect</string>
<string name="foreground_notification_no_devices">Hiçbir aygıta bağlı değil</string>
<string name="foreground_notification_devices">Bağlı: %s</string>
<string name="pref_plugin_telephony">Telefon bildiricisi</string>
<string name="pref_plugin_telephony_desc">Gelen aramalar için bildirim gönder</string>
<string name="pref_plugin_battery">Batarya raporu</string>
<string name="pref_plugin_battery_desc">Batarya durumunu belirli aralıklarla raporla</string>
<string name="pref_plugin_sftp">Dosya sistemi gösterme</string>
@@ -9,12 +13,16 @@
<string name="pref_plugin_clipboard_desc">Pano içeriğini paylaş</string>
<string name="pref_plugin_mousepad">Uzak girdi</string>
<string name="pref_plugin_mousepad_desc">Telefonunuzu veya tabletinizi, dokunmatik veya klavye olarak kullanın</string>
<string name="pref_plugin_presenter">Uzaktan slayt gösterisi</string>
<string name="pref_plugin_presenter_desc">Sunumdaki slaytları değiştirmek için aygıtınızı kullanın</string>
<string name="pref_plugin_remotekeyboard">Uzak tuşa basmaları getir</string>
<string name="pref_plugin_remotekeyboard_desc">Tuş basma eylemlerini, uzak aygıtlardan getir</string>
<string name="pref_plugin_mpris">Çoklu ortam denetimleri</string>
<string name="pref_plugin_mpris_desc">Ortam oynatıcınız için uzak denetim sağlar</string>
<string name="pref_plugin_runcommand">Komut Çalıştır</string>
<string name="pref_plugin_runcommand_desc">Uzak komutları, telefon veya tabletinizden tetikler</string>
<string name="pref_plugin_contacts">Rehber Eşitleyici</string>
<string name="pref_plugin_contacts_desc">Aygıtın rehberini eşitle</string>
<string name="pref_plugin_ping">Ping</string>
<string name="pref_plugin_ping_desc">Ping gönder ve al</string>
<string name="pref_plugin_notifications">Bildirim eşitleme</string>
@@ -29,6 +37,8 @@
<string name="cancel">İptal</string>
<string name="open_settings">Ayarları</string>
<string name="no_permissions">Bildirimler erişebilmek için izine ihtiyacınız var</string>
<string name="no_permission_mprisreceiver">Medya oynatıcılarınızı kontrol edebilmek için bildirimlere erişim izni vermeniz gerekir</string>
<string name="no_permissions_remotekeyboard">Tuşlara basmak için KDE Connect Uzak Klavye\'yi etkinleştirmeniz gerekir</string>
<string name="send_ping">Ping gönder</string>
<string name="open_mpris_controls">Çoklu ortam denetimi</string>
<string name="remotekeyboard_editing_only_title">Uzak tuşları, sadece düzenleme yaparken işle</string>
@@ -36,9 +46,11 @@
<string name="remotekeyboard_connected">Uzak klavye bağlantısı etkin</string>
<string name="remotekeyboard_multiple_connections">Birden çok uzak klavye bağlantısı mevcut, yapılandırmak istediğiniz aygıtı seçin</string>
<string name="open_mousepad">Girdi sil</string>
<string name="mousepad_info">İmleç kontrolü için parmağınızı ekranda hareket ettirin. Bir tıklama için hafifçe vurun, sağ ve orta düğmeler için iki/üç parmağınızı kullanın. Kaydırmak için 2 parmağınızı kullanın. Bırakıp sürüklemek için uzun basın.</string>
<string name="mousepad_double_tap_settings_title">İki parmak dokunma eylemini ayarla</string>
<string name="mousepad_triple_tap_settings_title">Üç parmak dokunma eylemini ayarla</string>
<string name="mousepad_sensitivity_settings_title">Dokunmatik yüzey hassasiyetini ayarla</string>
<string name="mousepad_acceleration_profile_settings_title">İşaretçi ivmesini ayarla</string>
<string name="mousepad_scroll_direction_title">Ters Kaydırma Yönü</string>
<string-array name="mousepad_tap_entries">
<item>Sağ tık</item>
@@ -53,12 +65,12 @@
<item>En Hızlı</item>
</string-array>
<string-array name="mousepad_acceleration_profile_entries">
<item>No Acceleration</item>
<item>Weakest</item>
<item>Weaker</item>
<item>Medium</item>
<item>Stronger</item>
<item>Strongest</item>
<item>Hızlandırıcı Yok</item>
<item>Zayıf</item>
<item>Güçsüz</item>
<item>Orta</item>
<item>Güçlü</item>
<item>Kuvvetli</item>
</string-array>
<string name="category_connected_devices">Bağlı aygıtlar</string>
<string name="category_not_paired_devices">Kullanılabilir aygıtlar</string>
@@ -84,7 +96,40 @@
<string name="pairing_request_from">%1s için eşleşme talebi</string>
<string name="received_url_title">%1s üzerinden bağlantı alındı</string>
<string name="received_url_text">\'%1s\' açmak için dokunun</string>
<plurals name="incoming_file_title">
<item quantity="one">%2$s içinden %1$d dosya alınıyor</item>
<item quantity="other">%2$s içinden %1$d dosyalar alınıyor</item>
</plurals>
<plurals name="incoming_files_text">
<item quantity="one">Dosya: %1s</item>
<item quantity="other">(Dosya %2$d %3$d) : %1$s</item>
</plurals>
<plurals name="outgoing_file_title">
<item quantity="one">%1$d dosya %2$s gönderiliyor</item>
<item quantity="other">%1$d dosyalar %2$s gönderiliyor</item>
</plurals>
<plurals name="outgoing_files_text">
<item quantity="one">Dosya: %1$s</item>
<item quantity="other">(Dosya %2$d %3$d) : %1$s</item>
</plurals>
<plurals name="received_files_title">
<item quantity="one">%1$s ögesinden alınan dosya</item>
<item quantity="other">%1$s içinden %2$d dosya alındı</item>
</plurals>
<plurals name="received_files_fail_title">
<item quantity="one">%1$s dosya alınamadı</item>
<item quantity="other">%1$s içindeki %2$d / %3$d dosya alınamadı</item>
</plurals>
<plurals name="sent_files_title">
<item quantity="one">Dosyayı şuraya gönder %1$s</item>
<item quantity="other">Gönder %2$d şuraya %1$s</item>
</plurals>
<plurals name="send_files_fail_title">
<item quantity="one">Dosya gönderilemedi %1$s</item>
<item quantity="other">Dosya gönderilemedi %2$d %3$d şuraya %1$s</item>
</plurals>
<string name="received_file_text">\'%1s\' açmak için dokunun</string>
<string name="cannot_create_file">Dosya oluşturulamıyor %s</string>
<string name="tap_to_answer">Cevap için dokunun</string>
<string name="reconnect">Yeniden Bağlan</string>
<string name="right_click">Sağ Tık Gönder</string>
@@ -96,7 +141,9 @@
<string name="pairing_reject">Reddet</string>
<string name="device">Aygıt</string>
<string name="pair_device">Aygıt eşleştir</string>
<string name="settings">Ayarlar</string>
<string name="mpris_play">Oynat</string>
<string name="mpris_pause">Duraklat</string>
<string name="mpris_previous">Önceki</string>
<string name="mpris_rew">Geri Sar</string>
<string name="mpris_ff">Hızlı İleri Sar</string>
@@ -104,6 +151,7 @@
<string name="mpris_volume">Ses</string>
<string name="mpris_settings">Çoklu Ortam Ayarları</string>
<string name="mpris_time_settings_title">İleri/geri düğmeleri</string>
<string name="mpris_time_settings_summary">Basıldığında hızlı ileri/geri sarma süresini ayarlayın</string>
<string-array name="mpris_time_entries">
<item>10 saniye</item>
<item>20 saniye</item>
@@ -111,6 +159,9 @@
<item>1 dakika</item>
<item>2 dakika</item>
</string-array>
<string name="mpris_notification_settings_title">Medya kontrol bildirimini göster</string>
<string name="mpris_notification_settings_summary">KDE Connect\'i açmadan medya oynatıcılarınızı kontrol etmenize izin verin</string>
<string name="share_to">Paylaş…</string>
<string name="protocol_version_older">Bu aygıt, eski bir protokol sürümü kullanıyor</string>
<string name="protocol_version_newer">Bu aygıt, daha yeni bir protokol sürümü kullanıyor</string>
<string name="general_settings">Genel Ayarlar</string>
@@ -124,12 +175,19 @@
<string name="pair_device_action">Yeni bir aygıt eşleştir</string>
<string name="unpair_device_action">Ayır %s</string>
<string name="custom_device_list">IP\'ye göre aygıtları ekle</string>
<string name="delete_custom_device">Sil %s?</string>
<string name="custom_device_deleted">Özel aygıt silindi</string>
<string name="custom_device_list_help">Cihazınız otomatik olarak algılanmazsa, İşlem Düğmesine tıklayarak IP adresini veya ana bilgisayar adını ekleyebilirsiniz</string>
<string name="custom_device_fab_hint">Aygıt ekle</string>
<string name="undo">Geri al</string>
<string name="share_notification_preference">Sesli bildirimler</string>
<string name="share_notification_preference_summary">Bir dosya alırken, ses çıkar ve titret</string>
<string name="share_destination_customize">Hedef dizini özelleştir</string>
<string name="share_destination_customize_summary_disabled">Gelen dosyalar İndirilenler\'de gözükecektir</string>
<string name="share_destination_customize_summary_enabled">Dosyalar aşağıdaki dizinden depolanacaktır</string>
<string name="share_destination_folder_preference">Hedef dizin</string>
<string name="share">Paylaş</string>
<string name="share_received_file">Paylaş \"%s\"</string>
<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>
@@ -137,7 +195,28 @@
<string name="sftp_sdcard">SD kart</string>
<string name="sftp_readonly">(salt okunur)</string>
<string name="sftp_camera">Kamera resimleri</string>
<string name="add_device_dialog_title">Aygıt ekle</string>
<string name="add_device_hint">Makine adı veya IP adresi</string>
<string name="sftp_preference_detected_sdcards">Algılanan SD kartlar</string>
<string name="sftp_preference_edit_sdcard_title">SD Kartı Düzenle</string>
<string name="sftp_preference_configured_storage_locations">Yapılandırılmış depolama yerleri</string>
<string name="sftp_preference_add_storage_location_title">Depolama yeri ekle</string>
<string name="sftp_preference_edit_storage_location">Depolama yerini düzenle</string>
<string name="sftp_preference_add_camera_shortcut">Kamera klasörü kısayolu ekle</string>
<string name="sftp_preference_add_camera_shortcut_summary_on">Kamera klasörüne kısayol ekle</string>
<string name="sftp_preference_add_camera_shortcut_summary_off">Kamera klasörüne kısayol eklemeyin</string>
<string name="sftp_storage_preference_storage_location">Depolama yeri</string>
<string name="sftp_storage_preference_storage_location_already_configured">Bu konum zaten yapılandırılmış</string>
<string name="sftp_storage_preference_click_to_select">seçmek için tıkla</string>
<string name="sftp_storage_preference_display_name">Ekran adı</string>
<string name="sftp_storage_preference_display_name_already_used">Bu görünen ad zaten kullanılıyor</string>
<string name="sftp_storage_preference_display_name_cannot_be_empty">Görünen ad boş olamaz</string>
<string name="sftp_action_mode_menu_delete">Sil</string>
<string name="sftp_no_sdcard_detected">SD kart algılanmadı</string>
<string name="sftp_no_storage_locations_configured">Yapılandırılmış depolama yeri yok</string>
<string name="sftp_saf_permission_explanation">Dosyalara uzaktan erişmek için depolama konumlarını yapılandırmanız gerekir</string>
<string name="add_host">Makine/IP ekle</string>
<string name="add_host_hint">Makine adı yada IP</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>
@@ -155,6 +234,7 @@
<string name="plugin_not_supported">Eklenti, aygıt tarafından desteklenmiyor</string>
<string name="findmyphone_title">Telefonumu bul</string>
<string name="findmyphone_title_tablet">Tabletimi bul</string>
<string name="findmyphone_title_tv">TV\'mi bul</string>
<string name="findmyphone_description">Aygıtı bulmak için onu çaldır</string>
<string name="findmyphone_found">Bulundu</string>
<string name="open"></string>
@@ -166,5 +246,59 @@
<string name="plugins_need_optional_permission">Bazı eklentilerin özellikleri, izin yetersizliğinden kapalı gelmektedir (daha fazla bilgi için dokunun):</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ünde telefon görüşmelerini görmek için telefon görüşmesi kayıtlarına ve telefon durumuna izin vermeniz gerekir</string>
<string name="telephony_optional_permission_explanation">Telefon numarası yerine kişi ismi görebilmek için telefonun kişilerine erişim gereklidir</string>
<string name="contacts_permission_explanation">Rehberinizi masaüstüyle paylaşmak için rehbere izin vermeniz gerekir</string>
<string name="select_ringtone">Bir zil sesi seç</string>
<string name="telephony_pref_blocked_title">Engellenen numaralar</string>
<string name="telephony_pref_blocked_dialog_desc">Bu numaralardan gelen aramaları ve SMS\'leri gösterme. Lütfen her satıra bir numara belirtin</string>
<string name="mpris_coverart_description">Güncel medyanın kapak resmi</string>
<string name="device_icon_description">Aygıt simgesi</string>
<string name="settings_icon_description">Ayarlar simgesi</string>
<string name="presenter_fullscreen">Tam ekran</string>
<string name="presenter_exit">Sunumdan çık</string>
<string name="presenter_lock_tip">Bir önceki/bir sonraki slayta geçmek için aygıtınızı kilitleyebilir ve ses seviyesi tuşlarını kullanabilirsiniz</string>
<string name="add_command">Komut ekle</string>
<string name="addcommand_explanation">Kayıtlı komut yok</string>
<string name="addcommand_explanation2">KDE Connect Sistem Ayarlarında yeni komutlar ekleyebilirsiniz</string>
<string name="add_command_description">Masaüstüne komut ekleyebilirsiniz</string>
<string name="pref_plugin_mprisreceiver">Medya Oynatıcı Kontrolü</string>
<string name="pref_plugin_mprisreceiver_desc">Telefonunuzun medya oynatıcılarını başka bir cihazdan kontrol edin</string>
<string name="dark_theme">Karanlık tema</string>
<string name="notification_channel_default">Diğer bildirimler</string>
<string name="notification_channel_persistent">Kalıcı gösterge</string>
<string name="notification_channel_media_control">Medya kontrolü</string>
<string name="notification_channel_filetransfer">Dosya aktarımı</string>
<string name="mpris_stop">Geçerli oynatıcıyı durdur</string>
<string name="copy_url_to_clipboard">URL\'yi panoya kopyala</string>
<string name="clipboard_toast">Panoya kopyalandı</string>
<string name="runcommand_notreachable">Aygıt erişilebilir değil</string>
<string name="runcommand_notpaired">Aygıt eşleştirilmedi</string>
<string name="runcommand_nosuchdevice">Böyle bir aygıt yok</string>
<string name="runcommand_noruncommandplugin">Bu cihazda Komut Çalıştır Eklentisi etkin değil</string>
<string name="pref_plugin_findremotedevice">Uzak aygıtı bul</string>
<string name="pref_plugin_findremotedevice_desc">Uzak aygıtı çaldır</string>
<string name="ring">Zil sesi</string>
<string name="pref_plugin_systemvolume">Sistem sesi</string>
<string name="pref_plugin_systemvolume_desc">Uzak cihazın sistem sesini kontrol et</string>
<string name="mute">Sessiz</string>
<string name="all">Tümü</string>
<string name="devices">Aygıtlar</string>
<string name="settings_rename">Aygıt adı</string>
<string name="settings_dark_mode">Karanlık tema</string>
<string name="settings_more_settings_title">Daha fazla ayar</string>
<string name="settings_more_settings_text">Aygıt başına ayarlar, bir aygıt içinden \'Eklenti ayarları\' altında bulunabilir.</string>
<string name="setting_persistent_notification">Kalıcı bildirim göster</string>
<string name="setting_persistent_notification_oreo">Kalıcı bildirim</string>
<string name="setting_persistent_notification_description">Bildirim ayarlarında etkinleştirmek/devre dışı bırakmak için dokun</string>
<string name="extra_options">Ek seçenekler</string>
<string name="privacy_options">Gizlilik seçenekleri</string>
<string name="set_privacy_options">Gizlilik seçeneklerinizi ayarlayın</string>
<string name="new_notification">Yeni bildirim</string>
<string name="block_contents">Bildirimlerin içeriğini engelle</string>
<string name="block_images">Bildirimlerde görüntüleri engelle</string>
<string name="notification_channel_receivenotification">Diğer aygıtlardan gelen bildirimler</string>
<string name="take_picture">Kamerayı başlat</string>
<string name="plugin_photo_desc">Fotoğraf çekmeyi ve aktarmayı kolaylaştırmak için kamera uygulamasını başlatın</string>
<string name="no_app_for_opening">Bu dosyayı açmak için uygun bir uygulama bulunamadı</string>
</resources>

View File

@@ -13,6 +13,7 @@
<string name="pref_plugin_clipboard_desc">共享剪贴板内容</string>
<string name="pref_plugin_mousepad">远程输入</string>
<string name="pref_plugin_mousepad_desc">将您的手机用或平板电脑用作触摸板和键盘</string>
<string name="pref_plugin_presenter">幻灯片遥控器</string>
<string name="pref_plugin_presenter_desc">使用移动设备切换幻灯片</string>
<string name="pref_plugin_remotekeyboard">接收远程按键</string>
<string name="pref_plugin_remotekeyboard_desc">从远程设备接收按键事件</string>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!--Set application-wide security config using base-config tag.-->
<base-config cleartextTrafficPermitted="true"/>
</network-security-config>

View File

@@ -421,6 +421,8 @@ public class Device implements BaseLink.PacketReceiver {
public void addLink(NetworkPacket identityPacket, BaseLink link) {
//FilesHelper.LogOpenFileCount();
links.add(link);
link.addPacketReceiver(this);
this.protocolVersion = identityPacket.getInt("protocolVersion");
@@ -448,9 +450,6 @@ public class Device implements BaseLink.PacketReceiver {
}
}
links.add(link);
try {
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context);
byte[] privateKeyBytes = Base64.decode(globalSettings.getString("privateKey", ""), 0);
@@ -499,7 +498,6 @@ public class Device implements BaseLink.PacketReceiver {
supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins());
}
link.addPacketReceiver(this);
reloadPluginsFromSettings();

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@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.Helpers;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
class ImagesHelper {
public static Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}

View File

@@ -20,9 +20,12 @@
package org.kde.kdeconnect.Helpers;
import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
@@ -32,17 +35,26 @@ import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@SuppressLint("InlinedApi")
public class SMSHelper {
/**
@@ -62,7 +74,6 @@ public class SMSHelper {
*/
@RequiresApi(Build.VERSION_CODES.KITKAT)
private static Uri getSMSURIGood() {
// TODO: Why not use Telephony.MmsSms.CONTENT_URI?
return Telephony.Sms.CONTENT_URI;
}
@@ -74,6 +85,21 @@ public class SMSHelper {
}
}
private static Uri getMMSUri() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return Telephony.Mms.CONTENT_URI;
} else {
// Same as with getSMSUriBad, this is unsafe if the manufacturer did their own thing
// before this was part of the API
return Uri.parse("content://mms/");
}
}
private static Uri getMMSPartUri() {
// Android says we should have Telephony.Mms.Part.CONTENT_URI. Alas, we do not.
return Uri.parse("content://mms/part/");
}
/**
* Get the base address for all message conversations
*/
@@ -87,6 +113,26 @@ public class SMSHelper {
}
}
@RequiresApi(api = Build.VERSION_CODES.FROYO)
private static Uri getCompleteConversationsUri() {
// This glorious - but completely undocumented - content URI gives us all messages, both MMS and SMS,
// in all conversations
// See https://stackoverflow.com/a/36439630/3723163
return Uri.parse("content://mms-sms/complete-conversations");
}
/**
* Column used to discriminate between SMS and MMS messages
* Unfortunately, this column is not defined for Telephony.MmsSms.CONTENT_CONVERSATIONS_URI
* (aka. content://mms-sms/conversations)
* which gives us the first message in every conversation, but it *is* defined for
* content://mms-sms/conversations/<threadID> which gives us the complete conversation matching
* that threadID, so at least it's partially useful to us.
*/
private static String getTransportTypeDiscriminatorColumn() {
return Telephony.MmsSms.TYPE_DISCRIMINATOR_COLUMN;
}
/**
* Get all the messages in a requested thread
*
@@ -94,53 +140,108 @@ public class SMSHelper {
* @param threadID Thread to look up
* @return List of all messages in the thread
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static List<Message> getMessagesInThread(Context context, ThreadID threadID) {
final String selection = ThreadID.lookupColumn + " == ?";
final String[] selectionArgs = new String[] { threadID.toString() };
public static @NonNull List<Message> getMessagesInThread(
@NonNull Context context,
@NonNull ThreadID threadID
) {
Uri uri = Uri.withAppendedPath(getConversationUri(), threadID.toString());
return getMessagesWithFilter(context, selection, selectionArgs);
return getMessages(uri, context, null, null, null, null);
}
/**
* Get all messages which have a timestamp after the requested timestamp
* Get the newest sent or received message
*
* This might have some potential for race conditions if many messages are received in a short
* timespan, but my target use-case is humans sending and receiving messages, so I don't think
* it will be an issue
*
* @param timestamp epoch in millis matching the timestamp to return
* @return null if no matching message is found, otherwise return a Message
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static List<Message> getMessagesSinceTimestamp(Context context, long timestamp) {
final String selection = Message.DATE + " > ?";
final String[] selectionArgs = new String[] {Long.toString(timestamp)};
public static @Nullable Message getNewestMessage(
@NonNull Context context
) {
List<Message> messages = getMessagesWithFilter(context, null, null, 1L);
return getMessagesWithFilter(context, selection, selectionArgs);
if (messages.size() > 1) {
Log.w("SMSHelper", "getNewestMessage asked for one message but got " + messages.size());
}
if (messages.size() < 1) {
return null;
} else {
return messages.get(0);
}
}
/**
* Gets Messages for caller functions, such as: getMessagesWithFilter() and getConversations()
* Gets messages which match the selection
*
* @param Uri Uri indicating the messages database to read
* @param uri Uri indicating the messages database to read
* @param context android.content.Context running the request.
* @param selection Parameterizable filter to use with the ContentResolver query. May be null.
* @param selectionArgs Parameters for selection. May be null.
* @return Returns HashMap<ThreadID, List<Message>>, which is transformed in caller functions into other classes.
* @param sortOrder Sort ordering passed to Android's content resolver. May be null for unspecified
* @param numberToGet Number of things to get from the result. Pass null to get all
* @return Returns List<Message> of all messages in the return set, either in the order of sortOrder or in an unspecified order
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static HashMap<ThreadID, List<Message>> getMessages(Uri Uri,
Context context,
String selection,
String[] selectionArgs) {
HashMap<ThreadID, List<Message>> toReturn = new HashMap<>();
try (Cursor myCursor = context.getContentResolver().query(
Uri,
Message.smsColumns,
private static @NonNull List<Message> getMessages(
@NonNull Uri uri,
@NonNull Context context,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder,
@Nullable Long numberToGet
) {
List<Message> toReturn = new ArrayList<>();
Set<String> allColumns = new HashSet<>();
allColumns.addAll(Arrays.asList(Message.smsColumns));
allColumns.addAll(Arrays.asList(Message.mmsColumns));
if (uri != getConversationUri()) {
// See https://issuetracker.google.com/issues/134592631
allColumns.add(getTransportTypeDiscriminatorColumn());
}
String[] fetchColumns = {};
fetchColumns = allColumns.toArray(fetchColumns);
try (Cursor myCursor = context.getContentResolver().query(
uri,
fetchColumns,
selection,
selectionArgs,
null)
sortOrder)
) {
if (myCursor != null && myCursor.moveToFirst()) {
int threadColumn = myCursor.getColumnIndexOrThrow(ThreadID.lookupColumn);
do {
int transportTypeColumn = myCursor.getColumnIndex(getTransportTypeDiscriminatorColumn());
TransportType transportType;
if (transportTypeColumn < 0) {
// The column didn't actually exist. See https://issuetracker.google.com/issues/134592631
// Try to determine using other information
int messageBoxColumn = myCursor.getColumnIndex(Telephony.Mms.MESSAGE_BOX);
// MessageBoxColumn is defined for MMS only
boolean messageBoxExists = !myCursor.isNull(messageBoxColumn);
if (messageBoxExists) {
transportType = TransportType.MMS;
} else {
// There is room here for me to have made an assumption and we'll guess wrong
// The penalty is the user will potentially get some garbled data, so that's not too bad.
transportType = TransportType.SMS;
}
} else {
String transportTypeString = myCursor.getString(transportTypeColumn);
if ("mms".equals(transportTypeString)) {
transportType = TransportType.MMS;
} else if ("sms".equals(transportTypeString)) {
transportType = TransportType.SMS;
} else {
Log.w("SMSHelper", "Skipping message with unknown TransportType: " + transportTypeString);
continue;
}
}
HashMap<String, String> messageInfo = new HashMap<>();
for (int columnIdx = 0; columnIdx < myCursor.getColumnCount(); columnIdx++) {
String colName = myCursor.getColumnName(columnIdx);
@@ -148,38 +249,41 @@ public class SMSHelper {
messageInfo.put(colName, body);
}
Message message = new Message(messageInfo);
ThreadID threadID = new ThreadID(message.threadID);
if (!toReturn.containsKey(threadID)) {
toReturn.put(threadID, new ArrayList<>());
if (transportType == TransportType.SMS) {
parseSMS(context, messageInfo);
} else if (transportType == TransportType.MMS) {
parseMMS(context, messageInfo);
}
toReturn.get(threadID).add(message);
} while (myCursor.moveToNext());
} else {
// No conversations or SMSes available?
Message message = new Message(messageInfo);
toReturn.add(message);
} while ((numberToGet == null || toReturn.size() != numberToGet) && myCursor.moveToNext());
}
} catch (SQLiteException e) {
throw new MessageAccessException(fetchColumns, uri, e);
}
return toReturn;
}
/**
* Get all messages matching the passed filter. See documentation for Android's ContentResolver
*
* @param context android.content.Context running the request
* @param selection Parameterizable filter to use with the ContentResolver query. May be null.
* @param selectionArgs Parameters for selection. May be null.
* @return List of messages matching the filter
* @param numberToGet Number of things to return. Pass null to get all
* @return List of messages matching the filter, from newest to oldest
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static List<Message> getMessagesWithFilter(Context context, String selection, String[] selectionArgs) {
HashMap<ThreadID, List<Message>> result = getMessages(SMSHelper.getSMSUri(), context, selection, selectionArgs);
List<Message> toReturn = new ArrayList<>();
private static List<Message> getMessagesWithFilter(
@NonNull Context context,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable Long numberToGet
) {
String sortOrder = Message.DATE + " DESC";
for(Map.Entry<ThreadID, List<Message>> entry : result.entrySet()) {
toReturn.addAll(entry.getValue());
}
return toReturn;
return getMessages(getCompleteConversationsUri(), context, selection, selectionArgs, sortOrder, numberToGet);
}
/**
@@ -189,28 +293,226 @@ public class SMSHelper {
* @param context android.content.Context running the request
* @return Mapping of thread_id to the first message in each thread
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static Map<ThreadID, Message> getConversations(Context context) {
HashMap<ThreadID, List<Message>> result = getMessages(SMSHelper.getConversationUri(), context, null, null);
HashMap<ThreadID, Message> toReturn = new HashMap<>();
public static Map<ThreadID, Message> getConversations(
@NonNull Context context
) {
Uri uri = SMSHelper.getConversationUri();
for(Map.Entry<ThreadID, List<Message>> entry : result.entrySet()) {
ThreadID returnThreadID = entry.getKey();
List<Message> messages = entry.getValue();
List<Message> unthreadedMessages = getMessages(uri, context, null, null, null, null);
toReturn.put(returnThreadID, messages.get(0));
Map<ThreadID, Message> toReturn = new HashMap<>();
for (Message message : unthreadedMessages) {
ThreadID tID = message.threadID;
if (toReturn.containsKey(tID)) {
Log.w("SMSHelper", "getConversations got two messages for the same ThreadID: " + tID);
}
toReturn.put(tID, message);
}
return toReturn;
}
private static void addEventFlag(
@NonNull Map<String, String> messageInfo,
@NonNull int eventFlag
) {
int oldEvent = Integer.parseInt(messageInfo.getOrDefault(Message.EVENT, "0"));
messageInfo.put(Message.EVENT, Integer.toString(oldEvent | eventFlag));
}
/**
* Do any parsing of an SMS message which still needs to be done
*/
private static void parseSMS(
@NonNull Context context,
@NonNull Map<String, String> messageInfo
) {
addEventFlag(messageInfo, Message.EVENT_TEXT_MESSAGE);
}
/**
* Parse all parts of the MMS message into the messageInfo format
* Original implementation from https://stackoverflow.com/a/6446831/3723163
*/
private static void parseMMS(
@NonNull Context context,
@NonNull Map<String, String> messageInfo
) {
addEventFlag(messageInfo, Message.EVENT_UNKNOWN);
String[] columns = {
Telephony.Mms.Part._ID, // The content ID of this part
Telephony.Mms.Part._DATA, // The location in the filesystem of the data
Telephony.Mms.Part.CONTENT_TYPE, // The mime type of the data
Telephony.Mms.Part.TEXT, // The plain text body of this MMS
Telephony.Mms.Part.CHARSET, // Charset of the plain text body
};
String mmsID = messageInfo.get(Message.U_ID);
String selection = Telephony.Mms.Part.MSG_ID + " = ?";
String[] selectionArgs = {mmsID};
// Get text body and attachments of the message
try (Cursor cursor = context.getContentResolver().query(
getMMSPartUri(),
columns,
selection,
selectionArgs,
null
)) {
if (cursor != null && cursor.moveToFirst()) {
int partIDColumn = cursor.getColumnIndexOrThrow(Telephony.Mms.Part._ID);
int contentTypeColumn = cursor.getColumnIndexOrThrow(Telephony.Mms.Part.CONTENT_TYPE);
int dataColumn = cursor.getColumnIndexOrThrow(Telephony.Mms.Part._DATA);
int textColumn = cursor.getColumnIndexOrThrow(Telephony.Mms.Part.TEXT);
// TODO: Parse charset (As usual, it is skimpily documented) (Possibly refer to MMS spec)
do {
Long partID = cursor.getLong(partIDColumn);
String contentType = cursor.getString(contentTypeColumn);
String data = cursor.getString(dataColumn);
if ("text/plain".equals(contentType)) {
String body;
if (data != null) {
// data != null means the data is on disk. Go get it.
body = getMmsText(context, partID);
} else {
body = cursor.getString(textColumn);
}
messageInfo.put(Message.BODY, body);
addEventFlag(messageInfo, Message.EVENT_TEXT_MESSAGE);
} //TODO: Parse more content types (photos and other attachments) here
} while (cursor.moveToNext());
}
}
// Determine whether the message was in- our out- bound
long messageBox = Long.parseLong(messageInfo.get(Telephony.Mms.MESSAGE_BOX));
if (messageBox == Telephony.Mms.MESSAGE_BOX_INBOX) {
messageInfo.put(Message.TYPE, Integer.toString(Telephony.Sms.MESSAGE_TYPE_INBOX));
} else if (messageBox == Telephony.Mms.MESSAGE_BOX_SENT) {
messageInfo.put(Message.TYPE, Integer.toString(Telephony.Sms.MESSAGE_TYPE_SENT));
} else {
// As an undocumented feature, it looks like the values of Mms.MESSAGE_BOX_*
// are the same as Sms.MESSAGE_TYPE_* of the same type. So by default let's just use
// the value we've got.
// This includes things like drafts, which are a far-distant plan to support
messageInfo.put(Message.TYPE, messageInfo.get(Telephony.Mms.MESSAGE_BOX));
}
// Get address(es) of the message
List<String> addresses = getMmsAddresses(context, Long.parseLong(mmsID));
// It looks like addresses[0] is always the sender of the message and
// following addresses are recipient(s)
// This usually means the addresses list is at least 2 long, but there are cases (special
// telco service messages) where it is not (only 1 long in that case, just the "sender")
// The address field which will get written to the message.
// Remember that this is always the address of the other side of the conversation
String address = "";
if (addresses.size() > 2) {
// TODO: Collect addresses for multi-target MMS
// Probably we will need to figure out the user's address at this point and strip it out of the list
addEventFlag(messageInfo, Message.EVENT_MULTI_TARGET);
} else {
if (messageBox == Telephony.Mms.MESSAGE_BOX_INBOX) {
address = addresses.get(0);
} else if (messageBox == Telephony.Mms.MESSAGE_BOX_SENT) {
address = addresses.get(1);
} else {
Log.w("SMSHelper", "Unknown message type " + messageBox + " while parsing addresses.");
// Not much smart to do here. Just leave as default.
}
}
messageInfo.put(Message.ADDRESS, address);
// Canonicalize the date field
// SMS uses epoch milliseconds, MMS uses epoch seconds. Standardize on milliseconds.
long rawDate = Long.parseLong(messageInfo.get(Message.DATE));
messageInfo.put(Message.DATE, Long.toString(rawDate * 1000));
}
/**
* Get the address(es) of an MMS message
* Original implementation from https://stackoverflow.com/a/6446831/3723163
*/
private static @NonNull List<String> getMmsAddresses(
@NonNull Context context,
@NonNull Long messageID
) {
Uri uri = ContentUris.appendId(getMMSUri().buildUpon(), messageID).appendPath("addr").build();
String[] columns = {
Telephony.Mms.Addr.MSG_ID, // ID of the message for which we are fetching addresses
Telephony.Mms.Addr.ADDRESS, // Address of this part
Telephony.Mms.Addr.CHARSET, // Charset of the returned address (where relevant) //TODO: Handle
};
String selection = Telephony.Mms.Addr.MSG_ID + " = ?";
String[] selectionArgs = {messageID.toString()};
List<String> addresses = new ArrayList<>();
try (Cursor addrCursor = context.getContentResolver().query(
uri,
columns,
selection,
selectionArgs,
null
)) {
if (addrCursor != null && addrCursor.moveToFirst()) {
int addressIndex = addrCursor.getColumnIndex(Telephony.Mms.Addr.ADDRESS);
do {
String address = addrCursor.getString(addressIndex);
addresses.add(address);
} while (addrCursor.moveToNext());
}
}
return addresses;
}
/**
* Get a text part of an MMS message
* Original implementation from https://stackoverflow.com/a/6446831/3723163
*/
private static String getMmsText(
@NonNull Context context,
@NonNull Long id
) {
Uri partURI = ContentUris.withAppendedId(getMMSPartUri(), id);
StringBuilder body = new StringBuilder();
try (InputStream is = context.getContentResolver().openInputStream(partURI)) {
if (is != null) {
InputStreamReader isr = new InputStreamReader(is, "UTF-8");
BufferedReader reader = new BufferedReader(isr);
String temp = reader.readLine();
while (temp != null) {
body.append(temp);
temp = reader.readLine();
}
}
} catch (IOException e) {
throw new SMSHelper.MessageAccessException(partURI, e);
}
return body.toString();
}
/**
* Register a ContentObserver for the Messages database
*
* @param observer ContentObserver to alert on Message changes
*/
public static void registerObserver(ContentObserver observer, Context context) {
public static void registerObserver(
@NonNull ContentObserver observer,
@NonNull Context context
) {
context.getContentResolver().registerContentObserver(
SMSHelper.getSMSUri(),
SMSHelper.getConversationUri(),
true,
observer
);
@@ -219,7 +521,6 @@ public class SMSHelper {
/**
* Represent an ID used to uniquely identify a message thread
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static class ThreadID {
final Long threadID;
static final String lookupColumn = Telephony.Sms.THREAD_ID;
@@ -244,10 +545,32 @@ public class SMSHelper {
}
}
/**
* Indicate that some error has occurred while reading a message.
* More useful for logging than catching and handling
*/
public static class MessageAccessException extends RuntimeException {
MessageAccessException(Uri uri, Throwable cause) {
super("Error getting messages from " + uri.toString(), cause);
}
MessageAccessException(String[] availableColumns, Uri uri, Throwable cause) {
super("Error getting messages from " + uri.toString() + " . Available columns were: " + Arrays.toString(availableColumns), cause);
}
}
/**
* Represent all known transport types
*/
public enum TransportType {
SMS,
MMS,
// Maybe in the future there will be more TransportType, but for now these are all I know about
}
/**
* Represent a message and all of its interesting data columns
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static class Message {
final String address;
@@ -255,8 +578,10 @@ public class SMSHelper {
public final long date;
final int type;
final int read;
final long threadID; // ThreadID is *int* for SMS messages but *long* for MMS
final int uID;
final ThreadID threadID; // ThreadID is *int* for SMS messages but *long* for MMS
final long uID;
final int event;
final int subscriptionID;
/**
* Named constants which are used to construct a Message
@@ -268,15 +593,19 @@ public class SMSHelper {
static final String TYPE = Telephony.Sms.TYPE; // Compare with Telephony.TextBasedSmsColumns.MESSAGE_TYPE_*
static final String READ = Telephony.Sms.READ; // Whether we have received a read report for this message (int)
static final String THREAD_ID = ThreadID.lookupColumn; // Magic number which binds (message) threads
static final String U_ID = Telephony.Sms._ID; // Something which uniquely identifies this message
static final String U_ID = Telephony.Sms._ID; // Something which uniquely identifies this message
static final String EVENT = "event";
static final String SUBSCRIPTION_ID = Telephony.Sms.SUBSCRIPTION_ID; // An ID which appears to identify a SIM card
/**
* Event flags
* A message should have a bitwise-or of event flags before delivering the packet
* Any events not supported by the receiving device should be ignored
*/
public static final int TEXT_MESSAGE = 0x1; // This message has a "body" field which contains
// pure, human-readable text
public static final int EVENT_UNKNOWN = 0x0; // The message was of some type we did not understand
public static final int EVENT_TEXT_MESSAGE = 0x1; // This message has a "body" field which contains
// pure, human-readable text
public static final int EVENT_MULTI_TARGET = 0x2; // Indicates that this message has multiple recipients
/**
* Define the columns which are to be extracted from the Android SMS database
@@ -289,6 +618,16 @@ public class SMSHelper {
Message.READ,
Message.THREAD_ID,
Message.U_ID,
Message.SUBSCRIPTION_ID,
};
static final String[] mmsColumns = new String[]{
Message.U_ID,
Message.THREAD_ID,
Message.DATE,
Message.READ,
Telephony.Mms.TEXT_ONLY,
Telephony.Mms.MESSAGE_BOX, // Compare with Telephony.BaseMmsColumns.MESSAGE_BOX_*
};
Message(final HashMap<String, String> messageInfo) {
@@ -298,15 +637,17 @@ public class SMSHelper {
if (messageInfo.get(Message.TYPE) == null)
{
// To be honest, I have no idea why this happens. The docs say the TYPE field is mandatory.
// Just stick some junk in here and hope we can figure it out later.
// Quick investigation suggests that these are multi-target MMSes
Log.w("SMSHelper", "Encountered undefined message type");
type = -1;
// Proceed anyway, maybe this is not an important problem.
} else {
type = Integer.parseInt(messageInfo.get(Message.TYPE));
}
read = Integer.parseInt(messageInfo.get(Message.READ));
threadID = Long.parseLong(messageInfo.get(Message.THREAD_ID));
threadID = new ThreadID(Long.parseLong(messageInfo.get(Message.THREAD_ID)));
uID = Integer.parseInt(messageInfo.get(Message.U_ID));
subscriptionID = Integer.parseInt(messageInfo.get(Message.SUBSCRIPTION_ID));
event = Integer.parseInt(messageInfo.get(Message.EVENT));
}
public JSONObject toJSONObject() throws JSONException {
@@ -319,6 +660,8 @@ public class SMSHelper {
json.put(Message.READ, read);
json.put(Message.THREAD_ID, threadID);
json.put(Message.U_ID, uID);
json.put(Message.SUBSCRIPTION_ID, subscriptionID);
json.put(Message.EVENT, event);
return json;
}

View File

@@ -29,8 +29,11 @@ import android.util.Log;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.RandomHelper;
import org.spongycastle.asn1.x500.RDN;
import org.spongycastle.asn1.x500.X500Name;
import org.spongycastle.asn1.x500.X500NameBuilder;
import org.spongycastle.asn1.x500.style.BCStyle;
import org.spongycastle.asn1.x500.style.IETFUtils;
import org.spongycastle.cert.X509CertificateHolder;
import org.spongycastle.cert.X509v3CertificateBuilder;
import org.spongycastle.cert.jcajce.JcaX509CertificateConverter;
@@ -62,6 +65,7 @@ import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.x500.X500Principal;
public class SslHelper {
@@ -80,12 +84,39 @@ public class SslHelper {
Log.e("SslHelper", "Error getting keys, can't create certificate");
return;
}
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
if (!settings.contains("certificate")) {
try {
String deviceId = DeviceHelper.getDeviceId(context);
boolean needsToGenerateCertificate = false;
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
if (settings.contains("certificate")) {
try {
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context);
byte[] certificateBytes = Base64.decode(globalSettings.getString("certificate", ""), 0);
X509CertificateHolder certificateHolder = new X509CertificateHolder(certificateBytes);
X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certificateHolder);
String certDeviceId = getCommonNameFromCertificate(cert);
if (!certDeviceId.equals(deviceId)) {
Log.e("KDE/SslHelper", "The certificate stored is from a different device id! (found: " + certDeviceId + " expected:" + deviceId + ")");
needsToGenerateCertificate = true;
} else {
certificate = cert;
}
} catch (Exception e) {
Log.e("KDE/SslHelper", "Exception reading own certificate", e);
needsToGenerateCertificate = true;
}
} else {
needsToGenerateCertificate = true;
}
if (needsToGenerateCertificate) {
Log.i("KDE/SslHelper", "Generating a certificate");
try {
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, DeviceHelper.getDeviceId(context));
nameBuilder.addRDN(BCStyle.CN, deviceId);
nameBuilder.addRDN(BCStyle.OU, "KDE Connect");
nameBuilder.addRDN(BCStyle.O, "KDE");
Calendar calendar = Calendar.getInstance();
@@ -107,20 +138,9 @@ public class SslHelper {
SharedPreferences.Editor edit = settings.edit();
edit.putString("certificate", Base64.encodeToString(certificate.getEncoded(), 0));
edit.apply();
} catch (Exception e) {
Log.e("KDE/initialiseCert", "Exception", e);
}
} else {
try {
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context);
byte[] certificateBytes = Base64.decode(globalSettings.getString("certificate", ""), 0);
X509CertificateHolder certificateHolder = new X509CertificateHolder(certificateBytes);
certificate = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certificateHolder);
} catch (Exception e) {
Log.e("KDE/SslHelper", "Exception reading own certificate", e);
}
}
}
@@ -247,4 +267,11 @@ public class SslHelper {
return new JcaX509CertificateConverter().setProvider(BC).getCertificate(certificateHolder);
}
private static String getCommonNameFromCertificate(X509Certificate cert) {
X500Principal principal = cert.getSubjectX500Principal();
X500Name x500name = new X500Name(principal.getName());
RDN rdn = x500name.getRDNs(BCStyle.CN)[0];
return IETFUtils.valueToString(rdn.getFirst().getValue());
}
}

View File

@@ -173,6 +173,13 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_mousepad, menu);
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> {
if (!plugin.isKeyboardEnabled()) {
menu.removeItem(R.id.menu_show_keyboard);
}
});
return true;
}

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.Plugins.MousePadPlugin;
@@ -36,6 +36,17 @@ public class MousePadPlugin extends Plugin {
//public final static String PACKET_TYPE_MOUSEPAD = "kdeconnect.mousepad";
public final static String PACKET_TYPE_MOUSEPAD_REQUEST = "kdeconnect.mousepad.request";
private final static String PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE = "kdeconnect.mousepad.keyboardstate";
private boolean keyboardEnabled = true;
@Override
public boolean onPacketReceived(NetworkPacket np) {
keyboardEnabled = np.getBoolean("state", true);
return true;
}
@Override
public String getDisplayName() {
@@ -71,7 +82,7 @@ public class MousePadPlugin extends Plugin {
@Override
public String[] getSupportedPacketTypes() {
return new String[0];
return new String[]{PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE};
}
@Override
@@ -135,4 +146,8 @@ public class MousePadPlugin extends Plugin {
device.sendPacket(np);
}
boolean isKeyboardEnabled() {
return keyboardEnabled;
}
}

View File

@@ -191,12 +191,12 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
}
private Pair<Device, MprisPlugin.MprisPlayer> findPlayer(BackgroundService service) {
//First try the previously displayed player
//First try the previously displayed player (if still playing)
if (notificationDevice != null && mprisDevices.contains(notificationDevice)) {
Device device = service.getDevice(notificationDevice);
if (device != null && device.isPluginEnabled("MprisPlugin")) {
if (shouldShowPlayer(notificationPlayer)){
if (shouldShowPlayer(notificationPlayer) && notificationPlayer.isPlaying()) {
return new Pair<>(device, notificationPlayer);
}
@@ -215,6 +215,17 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
return new Pair<>(otherDevice, player);
}
}
//So no player is playing. Try the previously displayed player again
if (notificationDevice != null && mprisDevices.contains(notificationDevice)) {
Device device = service.getDevice(notificationDevice);
if (device != null && device.isPluginEnabled("MprisPlugin")) {
if (shouldShowPlayer(notificationPlayer)) {
return new Pair<>(device, notificationPlayer);
}
}
}
return new Pair<>(null, null);
}
@@ -383,7 +394,7 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION);
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
PendingIntent piCloseNotification = PendingIntent.getActivity(service, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent piCloseNotification = PendingIntent.getBroadcast(service, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT);
notification.setDeleteIntent(piCloseNotification);
}
@@ -448,6 +459,9 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
//Clear the current player and media session
notificationPlayer = null;
if (mediaSession != null) {
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().build());
mediaSession.setMetadata(new MediaMetadataCompat.Builder().build());
mediaSession.setActive(false);
mediaSession.release();
mediaSession = null;
}

View File

@@ -34,6 +34,7 @@ import org.kde.kdeconnect.Helpers.AppsHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationReceiver;
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;
@@ -44,17 +45,16 @@ import java.util.List;
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 {
@PluginFactory.LoadablePlugin
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
public class MprisReceiverPlugin extends Plugin {
private final static String PACKET_TYPE_MPRIS = "kdeconnect.mpris";
private final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request";
private static final String TAG = "MprisReceiver";
private HashMap<String, MprisReceiverPlayer> players;
private MediaSessionChangeListener mediaSessionChangeListener;
@Override
public boolean onCreate() {
@@ -68,7 +68,9 @@ public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.O
if (null == manager)
return false;
manager.addOnActiveSessionsChangedListener(MprisReceiverPlugin.this, new ComponentName(context, NotificationReceiver.class), new Handler(Looper.getMainLooper()));
assert(mediaSessionChangeListener == null);
mediaSessionChangeListener = new MediaSessionChangeListener();
manager.addOnActiveSessionsChangedListener(mediaSessionChangeListener, new ComponentName(context, NotificationReceiver.class), new Handler(Looper.getMainLooper()));
createPlayers(manager.getActiveSessions(new ComponentName(context, NotificationReceiver.class)));
sendPlayerList();
@@ -83,8 +85,9 @@ public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.O
public void onDestroy() {
super.onDestroy();
MediaSessionManager manager = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
if (manager != null) {
manager.removeOnActiveSessionsChangedListener(MprisReceiverPlugin.this);
if (manager != null && mediaSessionChangeListener != null) {
manager.removeOnActiveSessionsChangedListener(mediaSessionChangeListener);
mediaSessionChangeListener = null;
}
}
@@ -169,18 +172,20 @@ public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.O
return new String[]{PACKET_TYPE_MPRIS};
}
@Override
public void onActiveSessionsChanged(@Nullable List<MediaController> controllers) {
private final class MediaSessionChangeListener implements MediaSessionManager.OnActiveSessionsChangedListener {
@Override
public void onActiveSessionsChanged(@Nullable List<MediaController> controllers) {
if (null == controllers) {
return;
}
players.clear();
createPlayers(controllers);
sendPlayerList();
if (null == controllers) {
return;
}
players.clear();
createPlayers(controllers);
sendPlayerList();
}
private void createPlayer(MediaController controller) {

View File

@@ -35,10 +35,16 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.text.SpannableString;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import org.json.JSONArray;
import org.kde.kdeconnect.Helpers.AppsHelper;
@@ -61,9 +67,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@PluginFactory.LoadablePlugin
public class NotificationsPlugin extends Plugin implements NotificationReceiver.NotificationListener {
@@ -73,7 +76,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
private final static String PACKET_TYPE_NOTIFICATION_REPLY = "kdeconnect.notification.reply";
private final static String PACKET_TYPE_NOTIFICATION_ACTION = "kdeconnect.notification.action";
private final static String TAG = "NotificationsPlugin";
private final static String TAG = "KDE/NotificationsPlugin";
private AppDatabase appDatabase;
@@ -157,7 +160,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
@Override
public void onNotificationRemoved(StatusBarNotification statusBarNotification) {
if (statusBarNotification == null) {
Log.w("onNotificationRemoved", "notification is null");
Log.w(TAG, "onNotificationRemoved: notification is null");
return;
}
String id = getNotificationKeyCompat(statusBarNotification);
@@ -191,7 +194,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
if (!appDatabase.isEnabled(statusBarNotification.getPackageName())) {
return;
// we dont want notification from this app
// we don't want notification from this app
}
String key = getNotificationKeyCompat(statusBarNotification);
@@ -221,85 +224,166 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION);
boolean isUpdate = currentNotifications.contains(key);
//If it's an update, the other end should have the icon already: no need to extract it and create the payload again
if (!isUpdate) {
//If it's an update, the other end should have the icon already: no need to extract it and create the payload again
try {
Bitmap appIcon;
Context foreignContext = context.createPackageContext(statusBarNotification.getPackageName(), 0);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
appIcon = iconToBitmap(foreignContext, notification.getLargeIcon());
} else {
appIcon = notification.largeIcon;
}
if (appIcon == null) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
appIcon = iconToBitmap(foreignContext, notification.getSmallIcon());
} else {
PackageManager pm = context.getPackageManager();
Resources foreignResources = pm.getResourcesForApplication(statusBarNotification.getPackageName());
Drawable foreignIcon = foreignResources.getDrawable(notification.icon);
appIcon = drawableToBitmap(foreignIcon);
}
}
if (appIcon != null && !appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_IMAGES)) {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
appIcon.compress(Bitmap.CompressFormat.PNG, 90, outStream);
byte[] bitmapData = outStream.toByteArray();
Log.e("PAYLOAD", "PAYLOAD: " + getChecksum(bitmapData));
np.setPayload(new NetworkPacket.Payload(bitmapData));
np.set("payloadHash", getChecksum(bitmapData));
}
} catch (Exception e) {
Log.e("NotificationsPlugin", "Error retrieving icon", e);
}
} else {
currentNotifications.add(key);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (notification.actions != null && notification.actions.length > 0) {
actions.put(key, new LinkedList<>());
JSONArray jsonArray = new JSONArray();
for (Notification.Action action : notification.actions) {
Bitmap appIcon = extractIcon(statusBarNotification, notification);
if (null == action.title)
continue;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH)
if (action.getRemoteInputs() != null && action.getRemoteInputs().length > 0)
continue;
jsonArray.put(action.title.toString());
actions.get(key).add(action);
}
np.set("actions", jsonArray);
if (appIcon != null && !appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_IMAGES)) {
attachIcon(np, appIcon);
}
}
np.set("actions", extractActions(notification, key));
np.set("id", key);
np.set("isClearable", statusBarNotification.isClearable());
np.set("appName", appName == null ? packageName : appName);
np.set("time", Long.toString(statusBarNotification.getPostTime()));
if (!appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_CONTENTS)) {
RepliableNotification rn = extractRepliableNotification(statusBarNotification);
if (rn.pendingIntent != null) {
if (rn != null) {
np.set("requestReplyId", rn.id);
pendingIntents.put(rn.id, rn);
}
np.set("ticker", getTickerText(notification));
np.set("title", getNotificationTitle(notification));
np.set("text", getNotificationText(notification));
Pair<String, String> conversation = extractConversation(notification);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (conversation.first != null) {
np.set("title", conversation.first);
} else {
np.set("title", notification.extras.getString(Notification.EXTRA_TITLE));
}
np.set("text", extractText(notification, conversation));
}
}
device.sendPacket(np);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private String extractText(Notification notification, Pair<String, String> conversation) {
if (conversation.second != null) {
return conversation.second;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && notification.extras.containsKey(Notification.EXTRA_BIG_TEXT)) {
return extractStringFromExtra(notification.extras, Notification.EXTRA_BIG_TEXT);
}
return notification.extras.getString(Notification.EXTRA_TEXT);
}
private void attachIcon(NetworkPacket np, Bitmap appIcon) {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
appIcon.compress(Bitmap.CompressFormat.PNG, 90, outStream);
byte[] bitmapData = outStream.toByteArray();
np.setPayload(new NetworkPacket.Payload(bitmapData));
np.set("payloadHash", getChecksum(bitmapData));
}
@Nullable
private Bitmap extractIcon(StatusBarNotification statusBarNotification, Notification notification) {
try {
Context foreignContext = context.createPackageContext(statusBarNotification.getPackageName(), 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notification.getLargeIcon() != null) {
return iconToBitmap(foreignContext, notification.getLargeIcon());
} else if (notification.largeIcon != null) {
return notification.largeIcon;
}
PackageManager pm = context.getPackageManager();
Resources foreignResources = pm.getResourcesForApplication(statusBarNotification.getPackageName());
Drawable foreignIcon = foreignResources.getDrawable(notification.icon);
return drawableToBitmap(foreignIcon);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Package not found", e);
}
return null;
}
@Nullable
private JSONArray extractActions(Notification notification, String key) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return null;
}
if (notification.actions == null || notification.actions.length == 0) {
return null;
}
actions.put(key, new LinkedList<>());
JSONArray jsonArray = new JSONArray();
for (Notification.Action action : notification.actions) {
if (null == action.title)
continue;
// Check whether it is a reply action. We have special treatment for them
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH)
if (action.getRemoteInputs() != null && action.getRemoteInputs().length > 0)
continue;
jsonArray.put(action.title.toString());
actions.get(key).add(action);
}
return jsonArray;
}
private Pair<String, String> extractConversation(Notification notification) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
return new Pair<>(null, null);
if (!notification.extras.containsKey(Notification.EXTRA_MESSAGES))
return new Pair<>(null, null);
Parcelable[] ms = notification.extras.getParcelableArray(Notification.EXTRA_MESSAGES);
if (ms == null)
return new Pair<>(null, null);
String title = notification.extras.getString(Notification.EXTRA_CONVERSATION_TITLE);
boolean isGroupConversation = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (notification.extras.containsKey(Notification.EXTRA_IS_GROUP_CONVERSATION)) {
isGroupConversation = notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION);
}
}
StringBuilder messagesBuilder = new StringBuilder();
for (Parcelable p : ms) {
Bundle m = (Bundle) p;
if (isGroupConversation) {
messagesBuilder.append(m.get("sender"));
messagesBuilder.append(": ");
}
messagesBuilder.append(m.getString("text"));
messagesBuilder.append("\n");
}
return new Pair<>(title, messagesBuilder.toString());
}
private Bitmap drawableToBitmap(Drawable drawable) {
if (drawable == null) return null;
@@ -328,13 +412,13 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
@RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH)
private void replyToNotification(String id, String message) {
if (pendingIntents.isEmpty() || !pendingIntents.containsKey(id)) {
Log.e("NotificationsPlugin", "No such notification");
Log.e(TAG, "No such notification");
return;
}
RepliableNotification repliableNotification = pendingIntents.get(id);
if (repliableNotification == null) {
Log.e("NotificationsPlugin", "No such notification");
Log.e(TAG, "No such notification");
return;
}
RemoteInput[] remoteInputs = new RemoteInput[repliableNotification.remoteInputs.size()];
@@ -344,7 +428,6 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
Bundle localBundle = new Bundle();
int i = 0;
for (RemoteInput remoteIn : repliableNotification.remoteInputs) {
getDetailsOfNotification(remoteIn);
remoteInputs[i] = remoteIn;
localBundle.putCharSequence(remoteInputs[i].getResultKey(), message);
i++;
@@ -354,91 +437,38 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
try {
repliableNotification.pendingIntent.send(context, 0, localIntent);
} catch (PendingIntent.CanceledException e) {
Log.e("NotificationPlugin", "replyToNotification error: " + e.getMessage());
Log.e(TAG, "replyToNotification error: " + e.getMessage());
}
pendingIntents.remove(id);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH)
private void getDetailsOfNotification(RemoteInput remoteInput) {
//Some more details of RemoteInput... no idea what for but maybe it will be useful at some point
String resultKey = remoteInput.getResultKey();
String label = remoteInput.getLabel().toString();
Boolean canFreeForm = remoteInput.getAllowFreeFormInput();
if (remoteInput.getChoices() != null && remoteInput.getChoices().length > 0) {
String[] possibleChoices = new String[remoteInput.getChoices().length];
for (int i = 0; i < remoteInput.getChoices().length; i++) {
possibleChoices[i] = remoteInput.getChoices()[i].toString();
}
}
}
private String getNotificationTitle(Notification notification) {
final String TITLE_KEY = "android.title";
final String TEXT_KEY = "android.text";
String title = "";
if (notification != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
Bundle extras = notification.extras;
title = extractStringFromExtra(extras, TITLE_KEY);
} catch (Exception e) {
Log.e("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText, e);
}
}
}
return title;
}
@Nullable
private RepliableNotification extractRepliableNotification(StatusBarNotification statusBarNotification) {
RepliableNotification repliableNotification = new RepliableNotification();
if (statusBarNotification != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try {
if (statusBarNotification.getNotification().actions != null) {
for (Notification.Action act : statusBarNotification.getNotification().actions) {
if (act != null && act.getRemoteInputs() != null) {
// Is a reply
repliableNotification.remoteInputs.addAll(Arrays.asList(act.getRemoteInputs()));
repliableNotification.pendingIntent = act.actionIntent;
break;
}
}
repliableNotification.packageName = statusBarNotification.getPackageName();
repliableNotification.tag = statusBarNotification.getTag();//TODO find how to pass Tag with sending PendingIntent, might fix Hangout problem
}
} catch (Exception e) {
Log.e("NotificationPlugin", "problem extracting notification wear for " + statusBarNotification.getNotification().tickerText, e);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return null;
}
if (statusBarNotification.getNotification().actions == null) {
return null;
}
for (Notification.Action act : statusBarNotification.getNotification().actions) {
if (act != null && act.getRemoteInputs() != null) {
// Is a reply
RepliableNotification repliableNotification = new RepliableNotification();
repliableNotification.remoteInputs.addAll(Arrays.asList(act.getRemoteInputs()));
repliableNotification.pendingIntent = act.actionIntent;
repliableNotification.packageName = statusBarNotification.getPackageName();
repliableNotification.tag = statusBarNotification.getTag(); //TODO find how to pass Tag with sending PendingIntent, might fix Hangout problem
return repliableNotification;
}
}
return repliableNotification;
return null;
}
private String getNotificationText(Notification notification) {
final String TEXT_KEY = "android.text";
String text = "";
if (notification != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
Bundle extras = notification.extras;
Object extraTextExtra = extras.get(TEXT_KEY);
if (extraTextExtra != null) text = extraTextExtra.toString();
} catch (Exception e) {
Log.e("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText, e);
}
}
}
return text;
}
private static String extractStringFromExtra(Bundle extras, String key) {
Object extra = extras.get(key);
if (extra == null) {
@@ -448,7 +478,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
} else if (extra instanceof SpannableString) {
return extra.toString();
} else {
Log.e("NotificationsPlugin", "Don't know how to extract text from extra of type: " + extra.getClass().getCanonicalName());
Log.e(TAG, "Don't know how to extract text from extra of type: " + extra.getClass().getCanonicalName());
return null;
}
}
@@ -459,32 +489,28 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
* instead the ticker text.
*/
private String getTickerText(Notification notification) {
final String TITLE_KEY = "android.title";
final String TEXT_KEY = "android.text";
String ticker = "";
if (notification != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
Bundle extras = notification.extras;
String extraTitle = extractStringFromExtra(extras, TITLE_KEY);
String extraText = extractStringFromExtra(extras, TEXT_KEY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
Bundle extras = notification.extras;
String extraTitle = extractStringFromExtra(extras, Notification.EXTRA_TITLE);
String extraText = extractStringFromExtra(extras, Notification.EXTRA_TEXT);
if (extraTitle != null && extraText != null && !extraText.isEmpty()) {
ticker = extraTitle + ": " + extraText;
} else if (extraTitle != null) {
ticker = extraTitle;
} else if (extraText != null) {
ticker = extraText;
}
} catch (Exception e) {
Log.e("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText, e);
if (extraTitle != null && extraText != null && !extraText.isEmpty()) {
ticker = extraTitle + ": " + extraText;
} else if (extraTitle != null) {
ticker = extraTitle;
} else if (extraText != null) {
ticker = extraText;
}
} catch (Exception e) {
Log.e(TAG, "problem parsing notification extras for " + notification.tickerText, e);
}
}
if (ticker.isEmpty()) {
ticker = (notification.tickerText != null) ? notification.tickerText.toString() : "";
}
if (ticker.isEmpty()) {
ticker = (notification.tickerText != null) ? notification.tickerText.toString() : "";
}
return ticker;
@@ -573,7 +599,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
} else {
int first = compatKey.indexOf(':');
if (first == -1) {
Log.e("cancelNotificationCompa", "Not formatted like a notification key: " + compatKey);
Log.e(TAG, "Not formatted like a notification key: " + compatKey);
return;
}
int last = compatKey.lastIndexOf(':');
@@ -616,7 +642,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
md.update(data);
return bytesToHex(md.digest());
} catch (NoSuchAlgorithmException e) {
Log.e("KDEConnect", "Error while generating checksum", e);
Log.e(TAG, "Error while generating checksum", e);
}
return null;
}

View File

@@ -34,7 +34,7 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import org.kde.kdeconnect.UserInterface.DeviceSettingsActivity;
import org.kde.kdeconnect.UserInterface.PluginSettingsActivity;
import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect_tp.R;
@@ -167,10 +167,10 @@ public class RemoteKeyboardService
if (instances.size() == 1) { // single instance of RemoteKeyboardPlugin -> access its settings
RemoteKeyboardPlugin plugin = instances.get(0);
if (plugin != null) {
Intent intent = new Intent(this, DeviceSettingsActivity.class);
Intent intent = new Intent(this, PluginSettingsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(DeviceSettingsActivity.EXTRA_DEVICE_ID, plugin.getDeviceId());
intent.putExtra(DeviceSettingsActivity.EXTRA_PLUGIN_KEY, plugin.getPluginKey());
intent.putExtra(PluginSettingsActivity.EXTRA_DEVICE_ID, plugin.getDeviceId());
intent.putExtra(PluginSettingsActivity.EXTRA_PLUGIN_KEY, plugin.getPluginKey());
startActivity(intent);
}
} else { // != 1 instance of plugin -> show main activity view

View File

@@ -22,6 +22,7 @@
package org.kde.kdeconnect.Plugins.SMSPlugin;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -54,17 +55,18 @@ import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import static org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin.PACKET_TYPE_TELEPHONY;
@PluginFactory.LoadablePlugin
@SuppressLint("InlinedApi")
public class SMSPlugin extends Plugin {
/**
@@ -181,7 +183,6 @@ public class SMSPlugin extends Plugin {
* In this case, this onChange expects to be called whenever *anything* in the Messages
* database changes and simply reports those updated messages to anyone who might be listening
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public void onChange(boolean selfChange) {
if (mPlugin.mostRecentTimestamp == 0) {
@@ -196,9 +197,9 @@ public class SMSPlugin extends Plugin {
long mostRecentTimestamp = mPlugin.mostRecentTimestamp;
mostRecentTimestampLock.unlock();
List<SMSHelper.Message> messages = SMSHelper.getMessagesSinceTimestamp(mPlugin.context, mostRecentTimestamp);
SMSHelper.Message message = SMSHelper.getNewestMessage(mPlugin.context);
if (messages.size() == 0) {
if (message.date <= mostRecentTimestamp) {
// Our onChange often gets called many times for a single message. Don't make unnecessary
// noise
return;
@@ -206,15 +207,11 @@ public class SMSPlugin extends Plugin {
// Update the most recent counter
mostRecentTimestampLock.lock();
for (SMSHelper.Message message : messages) {
if (message.date > mostRecentTimestamp) {
mPlugin.mostRecentTimestamp = message.date;
}
}
mPlugin.mostRecentTimestamp = message.date;
mostRecentTimestampLock.unlock();
// Send the alert about the update
device.sendPacket(constructBulkMessagePacket(messages));
device.sendPacket(constructBulkMessagePacket(Collections.singleton(message)));
}
}
@@ -275,7 +272,6 @@ public class SMSPlugin extends Plugin {
device.sendPacket(np);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public boolean onCreate() {
permissionExplanation = R.string.telepathy_permission_explanation;
@@ -301,7 +297,6 @@ public class SMSPlugin extends Plugin {
return context.getResources().getString(R.string.pref_plugin_telepathy_desc);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public boolean onPacketReceived(NetworkPacket np) {
@@ -344,7 +339,6 @@ public class SMSPlugin extends Plugin {
* @param messages Messages to include in the packet
* @return NetworkPacket of type PACKET_TYPE_SMS_MESSAGE
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static NetworkPacket constructBulkMessagePacket(Collection<SMSHelper.Message> messages) {
NetworkPacket reply = new NetworkPacket(PACKET_TYPE_SMS_MESSAGE);
@@ -354,8 +348,6 @@ public class SMSPlugin extends Plugin {
try {
JSONObject json = message.toJSONObject();
json.put("event", SMSHelper.Message.TEXT_MESSAGE);
body.put(json);
} catch (JSONException e) {
Log.e("Conversations", "Error serializing message");
@@ -373,7 +365,6 @@ public class SMSPlugin extends Plugin {
* <p>
* Send one packet of type PACKET_TYPE_SMS_MESSAGE with the first message in all conversations
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private boolean handleRequestConversations(NetworkPacket packet) {
Map<SMSHelper.ThreadID, SMSHelper.Message> conversations = SMSHelper.getConversations(this.context);
@@ -394,7 +385,6 @@ public class SMSPlugin extends Plugin {
return true;
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private boolean handleRequestConversation(NetworkPacket packet) {
SMSHelper.ThreadID threadID = new SMSHelper.ThreadID(packet.getLong("threadID"));
@@ -443,6 +433,9 @@ public class SMSPlugin extends Plugin {
};
}
/**
* I suspect we can actually go lower than this, but it might get unstable
*/
@Override
public int getMinSdk() {
return Build.VERSION_CODES.KITKAT;

View File

@@ -42,7 +42,7 @@ 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.PluginSettingsActivity;
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
import org.kde.kdeconnect_tp.R;
@@ -185,7 +185,7 @@ public class SftpSettingsFragment
private void restoreActionMode() {
try {
if (savedActionModeState.getBoolean(KEY_ACTION_MODE_ENABLED)) {
actionMode = ((DeviceSettingsActivity)requireActivity()).startSupportActionMode(this);
actionMode = ((PluginSettingsActivity)requireActivity()).startSupportActionMode(this);
if (actionMode != null) {
JSONArray jsonArray = savedActionModeState.getJSONArray(KEY_ACTION_MODE_SELECTED_ITEMS);
@@ -438,7 +438,7 @@ public class SftpSettingsFragment
@Override
public void onLongClick(StoragePreference storagePreference) {
if (actionMode == null) {
actionMode = ((DeviceSettingsActivity)requireActivity()).startSupportActionMode(this);
actionMode = ((PluginSettingsActivity)requireActivity()).startSupportActionMode(this);
if (actionMode != null) {
for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) {

View File

@@ -66,8 +66,7 @@ public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
lock = new Object();
networkPacketList = new ArrayList<>();
receiveNotification = new ReceiveNotification(device);
receiveNotification.addCancelAction(getId());
receiveNotification = new ReceiveNotification(device, getId());
currentFileNum = 0;
totalNumFiles = 0;
totalPayloadSize = 0;
@@ -190,7 +189,7 @@ public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
numFiles = totalNumFiles;
}
if (numFiles == 1 && currentNetworkPacket.has("open")) {
if (numFiles == 1 && currentNetworkPacket.getBoolean("open", false)) {
receiveNotification.cancel();
openFile(fileDocument);
} else {

View File

@@ -65,8 +65,7 @@ public class CompositeUploadFileJob extends BackgroundJob<Device, Void> {
totalPayloadSize = 0;
totalSend = 0;
prevProgressPercentage = 0;
uploadNotification = new UploadNotification(getDevice());
uploadNotification.addCancelAction(getId());
uploadNotification = new UploadNotification(getDevice(), getId());
sendPacketStatusCallback = new SendPacketStatusCallback();
}

View File

@@ -48,15 +48,16 @@ class ReceiveNotification {
private final int notificationId;
private NotificationCompat.Builder builder;
private final Device device;
private long currentJobId;
private long jobId;
//https://documentation.onesignal.com/docs/android-customizations#section-big-picture
private static final int bigImageWidth = 1440;
private static final int bigImageHeight = 720;
public ReceiveNotification(Device device) {
public ReceiveNotification(Device device, long jobId) {
this.device = device;
this.jobId = jobId;
notificationId = (int) System.currentTimeMillis();
notificationManager = (NotificationManager) device.getContext().getSystemService(Context.NOTIFICATION_SERVICE);
builder = new NotificationCompat.Builder(device.getContext(), NotificationHelper.Channels.FILETRANSFER)
@@ -64,6 +65,7 @@ class ReceiveNotification {
.setAutoCancel(true)
.setOngoing(true)
.setProgress(100, 0, true);
addCancelAction();
}
public void show() {
@@ -74,10 +76,8 @@ class ReceiveNotification {
notificationManager.cancel(notificationId);
}
public void addCancelAction(long jobId) {
builder.mActions.clear();
public void addCancelAction() {
currentJobId = jobId;
Intent cancelIntent = new Intent(device.getContext(), ShareBroadcastReceiver.class);
cancelIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
cancelIntent.setAction(SharePlugin.ACTION_CANCEL_SHARE);
@@ -88,12 +88,6 @@ class ReceiveNotification {
builder.addAction(R.drawable.ic_reject_pairing, device.getContext().getString(R.string.cancel), cancelPendingIntent);
}
public long getCurrentJobId() { return currentJobId; }
public int getNotificationId() {
return notificationId;
}
public void setTitle(String title) {
builder.setContentTitle(title);
builder.setTicker(title);

View File

@@ -39,9 +39,11 @@ class UploadNotification {
private NotificationCompat.Builder builder;
private final int notificationId;
private final Device device;
private long jobId;
UploadNotification(Device device) {
UploadNotification(Device device, long jobId) {
this.device = device;
this.jobId = jobId;
notificationId = (int) System.currentTimeMillis();
notificationManager = (NotificationManager) device.getContext().getSystemService(Context.NOTIFICATION_SERVICE);
@@ -50,11 +52,10 @@ class UploadNotification {
.setAutoCancel(true)
.setOngoing(true)
.setProgress(100, 0, true);
addCancelAction();
}
void addCancelAction(long jobId) {
builder.mActions.clear();
void addCancelAction() {
Intent cancelIntent = new Intent(device.getContext(), ShareBroadcastReceiver.class);
cancelIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
cancelIntent.setAction(SharePlugin.ACTION_CANCEL_SHARE);

View File

@@ -44,7 +44,7 @@ import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.NetworkHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.UserInterface.List.CustomItem;
import org.kde.kdeconnect.UserInterface.List.PluginListHeaderItem;
import org.kde.kdeconnect.UserInterface.List.FailedPluginListItem;
import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.List.PluginItem;
@@ -71,6 +71,8 @@ public class DeviceFragment extends Fragment {
private static final String ARG_DEVICE_ID = "deviceId";
private static final String ARG_FROM_DEVICE_LIST = "fromDeviceList";
private static final String TAG = "KDE/DeviceFragment";
private View rootView;
private String mDeviceId;
private Device device;
@@ -133,12 +135,10 @@ public class DeviceFragment extends Fragment {
setHasOptionsMenu(true);
//Log.e("DeviceFragment", "device: " + deviceId);
BackgroundService.RunCommand(mActivity, service -> {
device = service.getDevice(mDeviceId);
if (device == null) {
Log.e("DeviceFragment", "Trying to display a device fragment but the device is not present");
Log.e(TAG, "Trying to display a device fragment but the device is not present");
mActivity.onDeviceSelected(null);
return;
}
@@ -208,8 +208,6 @@ public class DeviceFragment extends Fragment {
@Override
public void onPrepareOptionsMenu(Menu menu) {
//Log.e("DeviceFragment", "onPrepareOptionsMenu");
super.onPrepareOptionsMenu(menu);
menu.clear();
@@ -231,7 +229,7 @@ public class DeviceFragment extends Fragment {
}
menu.add(R.string.device_menu_plugins).setOnMenuItemClickListener(menuItem -> {
Intent intent = new Intent(mActivity, DeviceSettingsActivity.class);
Intent intent = new Intent(mActivity, PluginSettingsActivity.class);
intent.putExtra("deviceId", mDeviceId);
startActivity(intent);
return true;
@@ -290,7 +288,6 @@ public class DeviceFragment extends Fragment {
}
private void refreshUI() {
//Log.e("DeviceFragment", "refreshUI");
if (device == null || rootView == null) {
return;
@@ -355,7 +352,7 @@ public class DeviceFragment extends Fragment {
} catch (IllegalStateException e) {
//Ignore: The activity was closed while we were trying to update it
} catch (ConcurrentModificationException e) {
Log.e("DeviceActivity", "ConcurrentModificationException");
Log.e(TAG, "ConcurrentModificationException");
this.run(); //Try again
}
@@ -404,31 +401,15 @@ public class DeviceFragment extends Fragment {
};
private void createPluginsList(ConcurrentHashMap<String, Plugin> plugins, int headerText, FailedPluginListItem.Action action) {
if (!plugins.isEmpty()) {
if (plugins.isEmpty())
return;
TextView header = new TextView(mActivity);
header.setPadding(
((int) (16 * getResources().getDisplayMetrics().density)),
((int) (28 * getResources().getDisplayMetrics().density)),
((int) (16 * getResources().getDisplayMetrics().density)),
((int) (8 * getResources().getDisplayMetrics().density))
);
header.setOnClickListener(null);
header.setOnLongClickListener(null);
header.setText(headerText);
pluginListItems.add(new CustomItem(header));
for (Map.Entry<String, Plugin> entry : plugins.entrySet()) {
String pluginKey = entry.getKey();
final Plugin plugin = entry.getValue();
if (device.isPluginEnabled(pluginKey)) {
if (plugin == null) {
pluginListItems.add(new SmallEntryItem(pluginKey));
} else {
pluginListItems.add(new FailedPluginListItem(plugin, action));
}
}
pluginListItems.add(new PluginListHeaderItem(headerText));
for (Plugin plugin : plugins.values()) {
if (!device.isPluginEnabled(plugin.getPluginKey())) {
continue;
}
pluginListItems.add(new FailedPluginListItem(plugin, action));
}
}
}

View File

@@ -53,10 +53,10 @@ public class DeviceSettingsAlertDialogFragment extends AlertDialogFragment {
setCallback(new Callback() {
@Override
public void onPositiveButtonClicked() {
Intent intent = new Intent(requireActivity(), DeviceSettingsActivity.class);
Intent intent = new Intent(requireActivity(), PluginSettingsActivity.class);
intent.putExtra(DeviceSettingsActivity.EXTRA_DEVICE_ID, deviceId);
intent.putExtra(DeviceSettingsActivity.EXTRA_PLUGIN_KEY, pluginKey);
intent.putExtra(PluginSettingsActivity.EXTRA_DEVICE_ID, deviceId);
intent.putExtra(PluginSettingsActivity.EXTRA_PLUGIN_KEY, pluginKey);
requireActivity().startActivity(intent);
}
});

View File

@@ -15,32 +15,35 @@
* 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.List;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
public class CustomItem implements ListAdapter.Item {
import org.kde.kdeconnect_tp.R;
private final View view;
public class PluginListHeaderItem implements ListAdapter.Item {
public CustomItem(View v) {
private final int text;
if (v == null)
throw new IllegalArgumentException("View must not be null");
this.view = v;
public PluginListHeaderItem(int text) {
this.text = text;
}
@NonNull
@Override
public View inflateView(LayoutInflater layoutInflater) {
return view;
TextView v = (TextView) layoutInflater.inflate(R.layout.list_item_plugin_header, null);
v.setText(text);
v.setOnClickListener(null);
v.setOnLongClickListener(null);
return v;
}
}

View File

@@ -32,7 +32,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
public class DeviceSettingsActivity
public class PluginSettingsActivity
extends AppCompatActivity
implements PluginPreference.PluginPreferenceCallback {
@@ -47,7 +47,7 @@ public class DeviceSettingsActivity
ThemeUtil.setUserPreferredTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_device_settings);
setContentView(R.layout.activity_plugin_settings);
if (getSupportActionBar() != null) {
getSupportActionBar().setDefaultDisplayHomeAsUpEnabled(true);
@@ -68,7 +68,7 @@ public class DeviceSettingsActivity
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragmentPlaceHolder);
if (fragment == null) {
if (pluginKey == null) {
fragment = DeviceSettingsFragment.newInstance(deviceId);
fragment = PluginSettingsListFragment.newInstance(deviceId);
} else {
Device device = BackgroundService.getInstance().getDevice(deviceId);
Plugin plugin = device.getPlugin(pluginKey);

View File

@@ -80,6 +80,6 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
}
public String getDeviceId() {
return ((DeviceSettingsActivity)requireActivity()).getDeviceId();
return ((PluginSettingsActivity)requireActivity()).getDeviceId();
}
}

View File

@@ -35,7 +35,7 @@ import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import androidx.recyclerview.widget.RecyclerView;
public class DeviceSettingsFragment extends PreferenceFragmentCompat {
public class PluginSettingsListFragment extends PreferenceFragmentCompat {
private static final String ARG_DEVICE_ID = "deviceId";
private static final String KEY_RECYCLERVIEW_LAYOUTMANAGER_STATE = "RecyclerViewLayoutmanagerState";
@@ -50,8 +50,8 @@ public class DeviceSettingsFragment extends PreferenceFragmentCompat {
*/
private boolean stateSaved;
public static DeviceSettingsFragment newInstance(@NonNull String deviceId) {
DeviceSettingsFragment fragment = new DeviceSettingsFragment();
public static PluginSettingsListFragment newInstance(@NonNull String deviceId) {
PluginSettingsListFragment fragment = new PluginSettingsListFragment();
Bundle args = new Bundle();
args.putString(ARG_DEVICE_ID, deviceId);