mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-09-01 06:35:09 +00:00
Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
af922a4277 | ||
|
48cccf3fca | ||
|
1c3389efa0 | ||
|
f54ebdb39b | ||
|
ea80000a4e | ||
|
d9db7e4ad9 | ||
|
1273cb641a | ||
|
cafbfcaee8 | ||
|
ac4c997efd | ||
|
d05feaa6d0 | ||
|
4967cc7a81 | ||
|
59cc3f2d4a | ||
|
0b9880d9b8 | ||
|
90f89c653d | ||
|
e53338c70e | ||
|
e641ff5a0a | ||
|
2ebaf6ae5b | ||
|
dd89463d75 | ||
|
7194b308cb | ||
|
328b708083 | ||
|
6404b86373 | ||
|
3263b37c8a | ||
|
a31476951a | ||
|
1d105bbb3d | ||
|
3154eef6a2 | ||
|
97a0389d04 | ||
|
8c1603f6e4 | ||
|
310e61b570 | ||
|
bfa4d05e0d | ||
|
6568bb486c | ||
|
a46fa23419 | ||
|
a29aeaad92 | ||
|
79744dc17b |
@@ -9,8 +9,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.kde.kdeconnect_tp"
|
||||
android:versionCode="12601"
|
||||
android:versionName="1.26.1">
|
||||
android:versionCode="12604"
|
||||
android:versionName="1.26.4">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
@@ -138,14 +138,6 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
|
||||
<receiver android:name="org.kde.kdeconnect.KdeConnectBroadcastReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||
|
||||
<data
|
||||
android:host="kdeconnect"
|
||||
android:path="/"
|
||||
android:scheme="package" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||
</intent-filter>
|
||||
|
12
fastlane/metadata/android/en-US/changelogs/12602.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/12602.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
1.26.2:
|
||||
* Fixed several bugs and crashes related to media controls.
|
||||
|
||||
1.26.1:
|
||||
* Fix infinite loop that would cause high CPU usage.
|
||||
|
||||
1.26.0:
|
||||
* Allow having different widgets for diferent devices.
|
||||
* Add stats about network packets sent and received.
|
||||
* Add the option to cancel a pairing request after sending it.
|
||||
* Fix device name set initially not being human-friendly.
|
||||
* Rewrite some of the internals to improve performance.
|
12
fastlane/metadata/android/en-US/changelogs/12603.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/12603.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
1.26.3:
|
||||
* Fixed several bugs and crashes related to media controls.
|
||||
|
||||
1.26.1:
|
||||
* Fix infinite loop that would cause high CPU usage.
|
||||
|
||||
1.26.0:
|
||||
* Allow having different widgets for diferent devices.
|
||||
* Add stats about network packets sent and received.
|
||||
* Add the option to cancel a pairing request after sending it.
|
||||
* Fix device name set initially not being human-friendly.
|
||||
* Rewrite some of the internals to improve performance.
|
12
fastlane/metadata/android/en-US/changelogs/12604.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/12604.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
1.26.4:
|
||||
* Fixed several bugs and crashes related to media controls.
|
||||
|
||||
1.26.1:
|
||||
* Fix infinite loop that would cause high CPU usage.
|
||||
|
||||
1.26.0:
|
||||
* Allow having different widgets for diferent devices.
|
||||
* Add stats about network packets sent and received.
|
||||
* Add the option to cancel a pairing request after sending it.
|
||||
* Fix device name set initially not being human-friendly.
|
||||
* Rewrite some of the internals to improve performance.
|
14
fastlane/metadata/android/eo/full_description.txt
Normal file
14
fastlane/metadata/android/eo/full_description.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
KDE Connect provizas aron da funkcioj por integri vian laborfluon trans aparatoj:
|
||||
|
||||
- Komuna tondujo: kopiu kaj algluu inter viaj aparatoj.
|
||||
- Kunhavigu dosierojn kaj URL-ojn al via komputilo de iu ajn aplikaĵo.
|
||||
- Ricevu sciigojn pri envenantaj vokoj kaj SMS-mesaĝoj en via komputilo.
|
||||
- Virtuala tuŝplato: Uzu vian telefonan ekranon kiel la tuŝplaton de via komputilo.
|
||||
- Sinkronigo de sciigoj: Legu viajn Android-sciigojn de la labortablo.
|
||||
- Plurmedia teleregilo: Uzu vian telefonon kiel teleregilon por Linuks-komunikilaj ludantoj.
|
||||
- WiFi-konekto: ne necesas USB-drato aŭ bluetooth.
|
||||
- Fin-al-fina TLS-ĉifrado: viaj informoj estas sekuraj.
|
||||
|
||||
Bonvolu noti, ke vi devos instali KDE Connect sur via komputilo por tiu aplikaĵo por funkcii, kaj tenu la labortablan version ĝisdatigita kun la Android versio por ke la plej novaj kapabloj funkciu.
|
||||
|
||||
Ĉi tiu programo estas parto de malfermkoda projekto kaj ĝi ekzistas danke al ĉiuj homoj kiuj kontribuis al ĝi. Vizitu la retejon por kapti la fontkodon.
|
1
fastlane/metadata/android/eo/short_description.txt
Normal file
1
fastlane/metadata/android/eo/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect integras vian poŝtelefonon kaj komputilon
|
1
fastlane/metadata/android/eo/title.txt
Normal file
1
fastlane/metadata/android/eo/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect
|
@@ -1 +1 @@
|
||||
KDE Connect integrates your smartphone and computer
|
||||
KDE Connect intègre votre téléphone et votre ordinateur.
|
61
po/eo/kdeconnect-android-store-full.po
Normal file
61
po/eo/kdeconnect-android-store-full.po
Normal file
@@ -0,0 +1,61 @@
|
||||
# translation of kdeconnect-android-store-full.pot to esperanto
|
||||
# Copyright (C) 2023 Free Software Foundation, Inc.
|
||||
# This file is distributed under the same license as the kdeconnect-android package.
|
||||
# Oliver Kellogg <okellogg@users.sourceforge.net, 2023.
|
||||
#
|
||||
#. extracted from ./metadata/android/en-US/full_description.txt
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: kdeconnect-android\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-07 00:47+0000\n"
|
||||
"PO-Revision-Date: 2023-07-06 06:18+0100\n"
|
||||
"Last-Translator: Oliver Kellogg <okellogg@users.sourceforge.net>\n"
|
||||
"Language-Team: Esperanto <kde-i18n-eo@kde.org>\n"
|
||||
"Language: eo\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
|
||||
msgid ""
|
||||
"KDE Connect provides a set of features to integrate your workflow across "
|
||||
"devices:\n"
|
||||
"\n"
|
||||
"- Shared clipboard: copy and paste between your devices.\n"
|
||||
"- Share files and URLs to your computer from any app.\n"
|
||||
"- Get notifications for incoming calls and SMS messages on your PC.\n"
|
||||
"- Virtual touchpad: Use your phone screen as your computer's touchpad.\n"
|
||||
"- Notifications sync: Read your Android notifications from the desktop.\n"
|
||||
"- Multimedia remote control: Use your phone as a remote for Linux media "
|
||||
"players.\n"
|
||||
"- WiFi connection: no USB wire or bluetooth needed.\n"
|
||||
"- End-to-end TLS encryption: your information is safe.\n"
|
||||
"\n"
|
||||
"Please note you will need to install KDE Connect on your computer for this "
|
||||
"app to work, and keep the desktop version up-to-date with the Android "
|
||||
"version for the latest features to work.\n"
|
||||
"\n"
|
||||
"This app is part of an open source project and it exists thanks to all the "
|
||||
"people who contributed to it. Visit the website to grab the source code."
|
||||
msgstr ""
|
||||
"KDE Connect provizas aron da funkcioj por integri vian laborfluon trans "
|
||||
"aparatoj:\n"
|
||||
"\n"
|
||||
"- Komuna tondujo: kopiu kaj algluu inter viaj aparatoj.\n"
|
||||
"- Kunhavigu dosierojn kaj URL-ojn al via komputilo de iu ajn aplikaĵo.\n"
|
||||
"- Ricevu sciigojn pri envenantaj vokoj kaj SMS-mesaĝoj en via komputilo.\n"
|
||||
"- Virtuala tuŝplato: Uzu vian telefonan ekranon kiel la tuŝplaton de via "
|
||||
"komputilo.\n"
|
||||
"- Sinkronigo de sciigoj: Legu viajn Android-sciigojn de la labortablo.\n"
|
||||
"- Plurmedia teleregilo: Uzu vian telefonon kiel teleregilon por Linuks-"
|
||||
"komunikilaj ludantoj.\n"
|
||||
"- WiFi-konekto: ne necesas USB-drato aŭ bluetooth.\n"
|
||||
"- Fin-al-fina TLS-ĉifrado: viaj informoj estas sekuraj.\n"
|
||||
"\n"
|
||||
"Bonvolu noti, ke vi devos instali KDE Connect sur via komputilo por tiu "
|
||||
"aplikaĵo por funkcii, kaj tenu la labortablan version ĝisdatigita kun la "
|
||||
"Android versio por ke la plej novaj kapabloj funkciu.\n"
|
||||
"\n"
|
||||
"Ĉi tiu programo estas parto de malfermkoda projekto kaj ĝi ekzistas danke al "
|
||||
"ĉiuj homoj kiuj kontribuis al ĝi. Vizitu la retejon por kapti la fontkodon."
|
22
po/eo/kdeconnect-android-store-short.po
Normal file
22
po/eo/kdeconnect-android-store-short.po
Normal file
@@ -0,0 +1,22 @@
|
||||
# translation of kdeconnect-android-store-short.pot to esperanto
|
||||
# Copyright (C) 2023 Free Software Foundation, Inc.
|
||||
# This file is distributed under the same license as the kdeconnect-android package.
|
||||
# Oliver Kellogg <okellogg@users.sourceforge.net, 2023.
|
||||
#
|
||||
#. extracted from ./metadata/android/en-US/short_description.txt
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: kdeconnect-android\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-07 00:47+0000\n"
|
||||
"PO-Revision-Date: 2023-07-05 07:30+0100\n"
|
||||
"Last-Translator: Oliver Kellogg <okellogg@users.sourceforge.net>\n"
|
||||
"Language-Team: Esperanto <kde-i18n-eo@kde.org>\n"
|
||||
"Language: eo\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
|
||||
msgid "KDE Connect integrates your smartphone and computer"
|
||||
msgstr "KDE Connect integras vian poŝtelefonon kaj komputilon"
|
@@ -1,17 +1,19 @@
|
||||
# Xavier BESNARD <xavier.besnard@neuf.fr>, 2023.
|
||||
#. extracted from ./metadata/android/en-US/short_description.txt
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: kdeconnect-android-store-short\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-07 00:47+0000\n"
|
||||
"PO-Revision-Date: 2023-06-08 05:31+0200\n"
|
||||
"Last-Translator: KDE Francophone <kde-francophone@kde.org>\n"
|
||||
"Language-Team: KDE Francophone <kde-francophone@kde.org>\n"
|
||||
"PO-Revision-Date: 2023-06-27 08:54+0200\n"
|
||||
"Last-Translator: Xavier BESNARD <xavier.besnard@neuf.fr>\n"
|
||||
"Language-Team: fr\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Generator: Lokalize 23.04.2\n"
|
||||
|
||||
msgid "KDE Connect integrates your smartphone and computer"
|
||||
msgstr ""
|
||||
msgstr "KDE Connect intègre votre téléphone et votre ordinateur."
|
||||
|
19
po/pt_BR/kdeconnect-android-store-short.po
Normal file
19
po/pt_BR/kdeconnect-android-store-short.po
Normal file
@@ -0,0 +1,19 @@
|
||||
# Luiz Fernando Ranghetti <elchevive@opensuse.org>, 2023.
|
||||
#. extracted from ./metadata/android/en-US/short_description.txt
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-07 00:47+0000\n"
|
||||
"PO-Revision-Date: 2023-06-26 17:19-0300\n"
|
||||
"Last-Translator: Luiz Fernando Ranghetti <elchevive@opensuse.org>\n"
|
||||
"Language-Team: Brazilian Portuguese <kde-i18n-pt_BR@kde.org>\n"
|
||||
"Language: pt_BR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Lokalize 22.12.3\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
msgid "KDE Connect integrates your smartphone and computer"
|
||||
msgstr "O KDE Connect integra seu celular e computador"
|
@@ -4,7 +4,7 @@ msgstr ""
|
||||
"Project-Id-Version: kdeorg\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-07 00:47+0000\n"
|
||||
"PO-Revision-Date: 2023-06-17 04:11\n"
|
||||
"PO-Revision-Date: 2023-07-03 11:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
|
@@ -4,7 +4,7 @@ msgstr ""
|
||||
"Project-Id-Version: kdeorg\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-07 00:47+0000\n"
|
||||
"PO-Revision-Date: 2023-06-17 04:11\n"
|
||||
"PO-Revision-Date: 2023-07-03 11:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Näytä hiiripainikkeet</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Aseta osoittimen kiihdytys</string>
|
||||
<string name="mousepad_scroll_direction_title">Käänteinen vierityssuunta</string>
|
||||
<string name="gyro_mouse_enabled_title">Käytä gyroskooppihiirtä</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Gyroskoopin herkkyys</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Vasen napsautus</item>
|
||||
<item>Oikea napsautus</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Napauta ja kirjoita</string>
|
||||
<string name="clear_compose">Tyhjennä</string>
|
||||
<string name="send_compose">Lähetä</string>
|
||||
<string name="compose_send_title">Kirjoita teksti</string>
|
||||
<string name="open_compose_send">Kirjoita teksti</string>
|
||||
<string name="about_kde_about"><h1>Tietoa</h1> <p>KDE on ohjelmoijien, taiteilijoiden, kirjoittajien, kääntäjien ja muiden sisällönluojien kansainvälinen yhteisö, joka on sitoutunut <a href=https://www.gnu.org/philosophy/free-sw.html>vapaiden ohjelmien</a> kehitykseen. KDE tuottaa Plasma-työpöytäympäristöä, satoja sovelluksia ja monia niitä tukevia ohjelmakirjastoja.</p> <p>KDE pyrkii yhteistyöhön: mikään yksittäinen toimija ei hallitse sen suuntaa tai tuotteita, vaan teemme yhdessä työtä yhteisen päämäärän hyväksi: tuottaaksemme maailman hienointa vapaata ohjelmistoa. Kaikki ovat tervetulleita <a href=https://community.kde.org/Get_Involved>liittymään ja avustamaan</a> KDE:ta – myös sinä.</p> Sivulta <a href=https://www.kde.org/>https://www.kde.org/</a> löytyy KDE-yhteisöstä ja tuottamistamme ohjelmista lisätietoa.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Ilmoita ohjelmavirheistä tai -toiveista</h1> <p>Ohjelmia voi aina parantaa, ja KDE-yhteisö on siihen valmis. Sinun – käyttäjän – on kuitenkin kerrottava meille, kun jokin ei toimi odotetusti tai voisi toimia paremmin.</p> <p>KDE:lla on virheenseurantajärjestelmä. Käy sivulla <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> tai käytä Tietoa-sivun painiketta ”Ilmoita ohjelmavirheestä”.</p> Parannusehdotuksissakin olet tervetullut käyttämään virheenseurantajärjestelmää kirjataksesi toiveesi. Varmista, että käytät vakavuustasoa ”Wishlist”.</string>
|
||||
|
@@ -396,4 +396,5 @@
|
||||
<string name="everyone_else">Toutes les autres personnes ayant contribué à « KDE Connect » depuis plusieurs années</string>
|
||||
<string name="send_clipboard">Envoyer le presse-papier</string>
|
||||
<string name="tap_to_execute">Tapotez pour lancer</string>
|
||||
<string name="plugin_stats">Statistiques des modules externes</string>
|
||||
</resources>
|
||||
|
@@ -108,6 +108,7 @@
|
||||
<string name="device_menu_plugins">Configuração dos plugins</string>
|
||||
<string name="device_menu_unpair">Cancelar emparelhamento</string>
|
||||
<string name="pair_new_device">Emparelhar novo dispositivo</string>
|
||||
<string name="cancel_pairing">Cancelar emparelhamento</string>
|
||||
<string name="unknown_device">Dispositivo desconhecido</string>
|
||||
<string name="error_not_reachable">Dispositivo inacessível</string>
|
||||
<string name="error_already_paired">Dispositivo já emparelhado</string>
|
||||
@@ -395,4 +396,5 @@
|
||||
<string name="everyone_else">Todos os outros que contribuíram para o KDE Connect ao longo dos anos</string>
|
||||
<string name="send_clipboard">Enviar para área de transferência</string>
|
||||
<string name="tap_to_execute">Toque para executar</string>
|
||||
<string name="plugin_stats">Estatísticas do plugin</string>
|
||||
</resources>
|
||||
|
@@ -381,7 +381,7 @@
|
||||
<string name="open_compose_send">Metin oluştur</string>
|
||||
<string name="about_kde_about"><h1>Hakkında</h1> <p>KDE, <a href=https://www.gnu.org/philosophy/free-sw.html>Özgür Yazılım</a> hareketine destek veren yazılım mühendislerinin, sanatçıların, yazarların, çevirmenlerin ve yaratıcıların bir araya geldiği dünya çapında bir topluluktur KDE, Plasma masaüstü ortamını, yüzlerce uygulamayı ve onları destekleyen sayısız yazılım kitaplığını üretir.</p> <p>KDE, işbirlikçi bir kurumdur: Tek bir varlık yönünü veya ürünlerini kontrol etmez. Bunun yerine, dünyanın en kaliteli Özgür Yazılım\'larını üretme hedefi için birlikte çalışırız. Herkes, sen de dahil olmak üzere, KDE\'ye <a href=https://community.kde.org/Get_Involved>katılıp katkıda bulunmakta özgürdür</a>.</p> KDE topluluğu ve ürettiğimiz yazılımlar hakkında daha fazla bilgi için <a href=https://www.kde.org/>https://www.kde.org/</a> adresini ziyaret edin.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Hataları veya İsteklerinizi Bildirin</h1> <p>Yazılım her zaman iyileştirilebilir ve KDE takımın bunu yapmaya hazır. Ancak siz de bir şey beklendiği gibi gitmezse veya hata verirse bize bildirin.</p> <p>KDE\'nin bir hata takip sistemi vardır. <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> adresini ziyaret edin veya hakkında ekranının \"Hata Bildir\" düğmesini kullanarak hataları bildirin.</p> Bir iyileştirme için öneriniz varsa bunu bildirmek için hata takip sistemini kullanabilirsiniz; yalnızca \"Wishlist\" ciddiyet düzeyini kullandığınızdan emin olun.</string>
|
||||
<string name="about_kde_join_kde">"<h1>KDE\'ye Katılın</h1> <p>KDE takımının bir üyesi olmak için yazılım geliştirici olmanıza gerek yok. Program arayüzlerini çeviren yerel takımlara katılabilirsiniz. Grafikler, temalar, sesler ve iyileştirilmiş belgelendirme sağlayabilirsiniz. Siz karar verin!</p> <p>Katılabileceğiniz bazı projeler hakkında bilgi almak için <a href=https://community.kde.org/Get_Involved>https://community.kde.org/Get_Involved</a> sayfasını ziyaret edin.</p> Daha fazla bilgiye veya belgeye gereksiniminiz varsa <a href=https://techbase.kde.org/>https://techbase.kde.org/</a> sayfasında aradığınızı bulabilirsiniz."</string>
|
||||
<string name="about_kde_join_kde">"<h1>KDE\'ye Katılın</h1> <p>KDE takımının bir üyesi olmak için yazılım geliştirici olmanıza gerek yok. Program arayüzlerini çeviren yerel takımlara katılabilirsiniz. Grafikler, temalar, sesler ve iyileştirilmiş belgelendirme sağlayabilirsiniz. Karar sizin!</p> <p>Katılabileceğiniz bazı projeler hakkında bilgi almak için <a href=https://community.kde.org/Get_Involved>https://community.kde.org/Get_Involved</a> sayfasını ziyaret edin.</p> Daha fazla bilgiye veya belgeye gereksiniminiz varsa <a href=https://techbase.kde.org/>https://techbase.kde.org/</a> sayfasında aradığınızı bulabilirsiniz."</string>
|
||||
<string name="about_kde_support_kde">"<h1>KDE\'yi Destekleyin</h1> <p>KDE yazılımları her zaman ücretsiz kalmayı sürdürecektir; ancak bunu oluşturmak bedava değildir. </p> <p>Geliştirmeyi desteklemek için KDE topluluğu, kar amacı gütmeyen bir kuruluş olan KDE e.V.\'yi kurmuştur, bu topluluk KDE topluğunu yasal ve finansal konularda temsil eder. KDE e.V. hakkında daha fazla bilgi için <a href=https://ev.kde.org/>https://ev.kde.org/</a> adresini ziyadet edin.</p> <p>KDE, finansal da dahil olmak üzere her türlü katkıdan yarar sağlar. Maddi kaynaklarımızla, geliştiricilerimizin ve diğerlerinin katkıda bulunurken oluşan masraflarını karşılıyoruz. Ayrıca yasal destek ve konferanslar ve toplantılar için de kullanılmaktadır.</p> <p>Emeklerimizi, finansal destekle desteklemeniz için <a href=https://www.kde.org/community/donations/>https://www.kde.org/community/donations/</a> adresinde bulunan yollardan birini kullanabilirsiniz.</p> Desteğiniz için şimdiden teşekkürler."</string>
|
||||
<string name="maintainer_and_developer">Projeyi sürdüren ve geliştirici</string>
|
||||
<string name="developer">Geliştirici</string>
|
||||
|
@@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceInfo;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -26,20 +27,20 @@ public abstract class BaseLink {
|
||||
|
||||
protected final Context context;
|
||||
private final BaseLinkProvider linkProvider;
|
||||
private final String deviceId;
|
||||
private final ArrayList<PacketReceiver> receivers = new ArrayList<>();
|
||||
|
||||
protected BaseLink(@NonNull Context context, @NonNull String deviceId, @NonNull BaseLinkProvider linkProvider) {
|
||||
protected BaseLink(@NonNull Context context, @NonNull BaseLinkProvider linkProvider) {
|
||||
this.context = context;
|
||||
this.linkProvider = linkProvider;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
/* To be implemented by each link for pairing handlers */
|
||||
public abstract String getName();
|
||||
|
||||
public abstract DeviceInfo getDeviceInfo();
|
||||
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
return getDeviceInfo().id;
|
||||
}
|
||||
|
||||
public BaseLinkProvider getLinkProvider() {
|
||||
@@ -54,7 +55,7 @@ public abstract class BaseLink {
|
||||
}
|
||||
|
||||
//Should be called from a background thread listening for packets
|
||||
protected void packetReceived(@NonNull NetworkPacket np) {
|
||||
public void packetReceived(@NonNull NetworkPacket np) {
|
||||
for(PacketReceiver pr : receivers) {
|
||||
pr.onPacketReceived(np);
|
||||
}
|
||||
|
@@ -8,23 +8,17 @@ package org.kde.kdeconnect.Backends;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public abstract class BaseLinkProvider {
|
||||
|
||||
private final CopyOnWriteArrayList<ConnectionReceiver> connectionReceivers = new CopyOnWriteArrayList<>();
|
||||
|
||||
public interface ConnectionReceiver {
|
||||
void onConnectionReceived(@NonNull final String deviceId,
|
||||
@NonNull final Certificate certificate,
|
||||
@NonNull final NetworkPacket identityPacket,
|
||||
@NonNull final BaseLink link);
|
||||
void onConnectionReceived(@NonNull final BaseLink link);
|
||||
void onConnectionLost(BaseLink link);
|
||||
}
|
||||
|
||||
private final CopyOnWriteArrayList<ConnectionReceiver> connectionReceivers = new CopyOnWriteArrayList<>();
|
||||
|
||||
public void addConnectionReceiver(ConnectionReceiver cr) {
|
||||
connectionReceivers.add(cr);
|
||||
}
|
||||
@@ -36,13 +30,10 @@ public abstract class BaseLinkProvider {
|
||||
/**
|
||||
* To be called from the child classes when a link to a new device is established
|
||||
*/
|
||||
protected void onConnectionReceived(@NonNull final String deviceId,
|
||||
@NonNull final Certificate certificate,
|
||||
@NonNull final NetworkPacket identityPacket,
|
||||
@NonNull final BaseLink link) {
|
||||
protected void onConnectionReceived(@NonNull final BaseLink link) {
|
||||
//Log.i("KDE/LinkProvider", "onConnectionReceived");
|
||||
for(ConnectionReceiver cr : connectionReceivers) {
|
||||
cr.onConnectionReceived(deviceId, certificate, identityPacket, link);
|
||||
cr.onConnectionReceived(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,7 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceInfo;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -34,6 +35,7 @@ public class BluetoothLink extends BaseLink {
|
||||
private final OutputStream output;
|
||||
private final BluetoothDevice remoteAddress;
|
||||
private final BluetoothLinkProvider linkProvider;
|
||||
private final DeviceInfo deviceInfo;
|
||||
|
||||
private boolean continueAccepting = true;
|
||||
|
||||
@@ -93,11 +95,12 @@ public class BluetoothLink extends BaseLink {
|
||||
}
|
||||
});
|
||||
|
||||
public BluetoothLink(Context context, ConnectionMultiplexer connection, InputStream input, OutputStream output, BluetoothDevice remoteAddress, String deviceId, BluetoothLinkProvider linkProvider) {
|
||||
super(context, deviceId, linkProvider);
|
||||
public BluetoothLink(Context context, ConnectionMultiplexer connection, InputStream input, OutputStream output, BluetoothDevice remoteAddress, DeviceInfo deviceInfo, BluetoothLinkProvider linkProvider) {
|
||||
super(context, linkProvider);
|
||||
this.connection = connection;
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
this.deviceInfo = deviceInfo;
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.linkProvider = linkProvider;
|
||||
}
|
||||
@@ -111,6 +114,11 @@ public class BluetoothLink extends BaseLink {
|
||||
return "BluetoothLink";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceInfo getDeviceInfo() {
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
if (connection == null) {
|
||||
return;
|
||||
@@ -120,7 +128,7 @@ public class BluetoothLink extends BaseLink {
|
||||
connection.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
linkProvider.disconnectedLink(this, getDeviceId(), remoteAddress);
|
||||
linkProvider.disconnectedLink(this, remoteAddress);
|
||||
}
|
||||
|
||||
private void sendMessage(NetworkPacket np) throws JSONException, IOException {
|
||||
|
@@ -20,6 +20,8 @@ import android.util.Log;
|
||||
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceInfo;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
@@ -54,10 +56,6 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
|
||||
|
||||
private void addLink(NetworkPacket identityPacket, BluetoothLink link) throws CertificateException {
|
||||
String deviceId = identityPacket.getString("deviceId");
|
||||
String certificateString = identityPacket.getString("certificate");
|
||||
byte[] certificateBytes = Base64.decode(certificateString, 0);
|
||||
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
|
||||
|
||||
Log.i("BluetoothLinkProvider", "addLink to " + deviceId);
|
||||
BluetoothLink oldLink = visibleDevices.get(deviceId);
|
||||
if (oldLink == link) {
|
||||
@@ -65,8 +63,9 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
|
||||
return;
|
||||
}
|
||||
visibleDevices.put(deviceId, link);
|
||||
onConnectionReceived(deviceId, certificate, identityPacket, link);
|
||||
onConnectionReceived(link);
|
||||
link.startListening();
|
||||
link.packetReceived(identityPacket);
|
||||
if (oldLink != null) {
|
||||
Log.i("BluetoothLinkProvider", "Removing old connection to same device");
|
||||
oldLink.disconnect();
|
||||
@@ -127,9 +126,9 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
|
||||
return "BluetoothLinkProvider";
|
||||
}
|
||||
|
||||
public void disconnectedLink(BluetoothLink link, String deviceId, BluetoothDevice remoteAddress) {
|
||||
public void disconnectedLink(BluetoothLink link, BluetoothDevice remoteAddress) {
|
||||
sockets.remove(remoteAddress);
|
||||
visibleDevices.remove(deviceId);
|
||||
visibleDevices.remove(link.getDeviceId());
|
||||
onConnectionLost(link);
|
||||
}
|
||||
|
||||
@@ -196,8 +195,10 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
|
||||
OutputStream outputStream = connection.getDefaultOutputStream();
|
||||
InputStream inputStream = connection.getDefaultInputStream();
|
||||
|
||||
NetworkPacket np = NetworkPacket.createIdentityPacket(context);
|
||||
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
|
||||
NetworkPacket np = myDeviceInfo.toIdentityPacket();
|
||||
np.set("certificate", Base64.encodeToString(SslHelper.certificate.getEncoded(), 0));
|
||||
|
||||
byte[] message = np.serialize().getBytes(Charsets.UTF_8);
|
||||
outputStream.write(message);
|
||||
outputStream.flush();
|
||||
@@ -223,9 +224,15 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
|
||||
|
||||
Log.i("BTLinkProvider/Server", "Received identity packet");
|
||||
|
||||
String certificateString = identityPacket.getString("certificate");
|
||||
byte[] certificateBytes = Base64.decode(certificateString, 0);
|
||||
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
|
||||
|
||||
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
|
||||
|
||||
BluetoothLink link = new BluetoothLink(context, connection,
|
||||
inputStream, outputStream, socket.getRemoteDevice(),
|
||||
identityPacket.getString("deviceId"), BluetoothLinkProvider.this);
|
||||
deviceInfo, BluetoothLinkProvider.this);
|
||||
addLink(identityPacket, link);
|
||||
} catch (Exception e) {
|
||||
synchronized (sockets) {
|
||||
@@ -360,7 +367,7 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
|
||||
|
||||
Log.i("BTLinkProvider/Client", "Received identity packet");
|
||||
|
||||
String myId = NetworkPacket.createIdentityPacket(context).getString("deviceId");
|
||||
String myId = DeviceHelper.getDeviceId(context);
|
||||
if (identityPacket.getString("deviceId").equals(myId)) {
|
||||
// Probably won't happen, but just to be safe
|
||||
connection.close();
|
||||
@@ -373,10 +380,18 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
|
||||
|
||||
Log.i("BTLinkProvider/Client", "identity packet received, creating link");
|
||||
|
||||
final BluetoothLink link = new BluetoothLink(context, connection, inputStream, outputStream,
|
||||
socket.getRemoteDevice(), identityPacket.getString("deviceId"), BluetoothLinkProvider.this);
|
||||
String certificateString = identityPacket.getString("certificate");
|
||||
byte[] certificateBytes = Base64.decode(certificateString, 0);
|
||||
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
|
||||
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
|
||||
|
||||
final BluetoothLink link = new BluetoothLink(context, connection, inputStream, outputStream,
|
||||
socket.getRemoteDevice(), deviceInfo, BluetoothLinkProvider.this);
|
||||
|
||||
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
|
||||
NetworkPacket np2 = myDeviceInfo.toIdentityPacket();
|
||||
np2.set("certificate", Base64.encodeToString(SslHelper.certificate.getEncoded(), 0));
|
||||
|
||||
NetworkPacket np2 = NetworkPacket.createIdentityPacket(context);
|
||||
link.sendPacket(np2, new Device.SendPacketStatusCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
|
@@ -16,6 +16,7 @@ import org.json.JSONObject;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceInfo;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
@@ -42,6 +43,8 @@ public class LanLink extends BaseLink {
|
||||
Locally, Remotely
|
||||
}
|
||||
|
||||
private final DeviceInfo deviceInfo;
|
||||
|
||||
private volatile SSLSocket socket = null;
|
||||
|
||||
@Override
|
||||
@@ -99,8 +102,9 @@ public class LanLink extends BaseLink {
|
||||
return oldSocket;
|
||||
}
|
||||
|
||||
public LanLink(Context context, String deviceId, BaseLinkProvider linkProvider, SSLSocket socket) throws IOException {
|
||||
super(context, deviceId, linkProvider);
|
||||
public LanLink(@NonNull Context context, @NonNull DeviceInfo deviceInfo, @NonNull BaseLinkProvider linkProvider, @NonNull SSLSocket socket) throws IOException {
|
||||
super(context, linkProvider);
|
||||
this.deviceInfo = deviceInfo;
|
||||
reset(socket);
|
||||
}
|
||||
|
||||
@@ -109,6 +113,11 @@ public class LanLink extends BaseLink {
|
||||
return "LanLink";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceInfo getDeviceInfo() {
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Override
|
||||
public boolean sendPacket(@NonNull NetworkPacket np, @NonNull final Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {
|
||||
|
@@ -17,6 +17,7 @@ import org.json.JSONException;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceInfo;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper;
|
||||
@@ -50,7 +51,7 @@ import kotlin.text.Charsets;
|
||||
/**
|
||||
* This LanLinkProvider creates {@link LanLink}s to other devices on the same
|
||||
* WiFi network. The first packet sent over a socket must be an
|
||||
* {@link NetworkPacket#createIdentityPacket(Context)}.
|
||||
* {@link DeviceInfo#toIdentityPacket()}.
|
||||
*
|
||||
* @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted)
|
||||
*/
|
||||
@@ -128,13 +129,19 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
Log.i("KDE/LanLinkProvider", "Broadcast identity packet received from " + identityPacket.getString("deviceName"));
|
||||
|
||||
int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT);
|
||||
if (tcpPort < MIN_PORT || tcpPort > MAX_PORT) {
|
||||
Log.e("LanLinkProvider", "TCP port outside of kdeconnect's range");
|
||||
return;
|
||||
}
|
||||
|
||||
SocketFactory socketFactory = SocketFactory.getDefault();
|
||||
Socket socket = socketFactory.createSocket(address, tcpPort);
|
||||
configureSocket(socket);
|
||||
|
||||
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
|
||||
NetworkPacket myIdentity = myDeviceInfo.toIdentityPacket();
|
||||
|
||||
OutputStream out = socket.getOutputStream();
|
||||
NetworkPacket myIdentity = NetworkPacket.createIdentityPacket(context);
|
||||
out.write(myIdentity.serialize().getBytes());
|
||||
out.flush();
|
||||
|
||||
@@ -190,17 +197,19 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
identityPacketReceived(identityPacket, socket, connectionStarted);
|
||||
}
|
||||
|
||||
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted);
|
||||
String deviceName = identityPacket.getString("deviceName", "unknown");
|
||||
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + deviceName + " trusted:" + isDeviceTrusted);
|
||||
|
||||
final SSLSocket sslSocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode);
|
||||
sslSocket.addHandshakeCompletedListener(event -> {
|
||||
String mode = clientMode ? "client" : "server";
|
||||
try {
|
||||
Certificate certificate = event.getPeerCertificates()[0];
|
||||
Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + identityPacket.getString("deviceName") + " secured with " + event.getCipherSuite());
|
||||
addLink(deviceId, certificate, identityPacket, sslSocket);
|
||||
} catch (Exception e) {
|
||||
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e);
|
||||
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
|
||||
Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + deviceName + " secured with " + event.getCipherSuite());
|
||||
addLink(sslSocket, deviceInfo);
|
||||
} catch (IOException e) {
|
||||
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + deviceName, e);
|
||||
Device device = KdeConnect.getInstance().getDevice(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
@@ -218,24 +227,26 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
/**
|
||||
* Add or update a link in the {@link #visibleDevices} map.
|
||||
*
|
||||
* @param deviceId remote device id
|
||||
* @param certificate remote device certificate
|
||||
* @param identityPacket identity packet with the remote device's device name, type, protocol version, etc.
|
||||
* @param socket a new Socket, which should be used to send and receive packets from the remote device
|
||||
* @param deviceInfo remote device info
|
||||
* @throws IOException if an exception is thrown by {@link LanLink#reset(SSLSocket)}
|
||||
*/
|
||||
private void addLink(String deviceId, Certificate certificate, final NetworkPacket identityPacket, SSLSocket socket) throws IOException {
|
||||
LanLink currentLink = visibleDevices.get(deviceId);
|
||||
if (currentLink != null) {
|
||||
//Update old link
|
||||
Log.i("KDE/LanLinkProvider", "Reusing same link for device " + deviceId);
|
||||
final Socket oldSocket = currentLink.reset(socket);
|
||||
private void addLink(SSLSocket socket, DeviceInfo deviceInfo) throws IOException {
|
||||
LanLink link = visibleDevices.get(deviceInfo.id);
|
||||
if (link != null) {
|
||||
if (!link.getDeviceInfo().certificate.equals(deviceInfo.certificate)) {
|
||||
Log.e("LanLinkProvider", "LanLink was asked to replace a socket but the certificate doesn't match, aborting");
|
||||
return;
|
||||
}
|
||||
// Update existing link
|
||||
Log.d("KDE/LanLinkProvider", "Reusing same link for device " + deviceInfo.id);
|
||||
final Socket oldSocket = link.reset(socket);
|
||||
} else {
|
||||
Log.i("KDE/LanLinkProvider", "Creating a new link for device " + deviceId);
|
||||
//Let's create the link
|
||||
LanLink link = new LanLink(context, deviceId, this, socket);
|
||||
visibleDevices.put(deviceId, link);
|
||||
onConnectionReceived(deviceId, certificate, identityPacket, link);
|
||||
// Create a new link
|
||||
Log.d("KDE/LanLinkProvider", "Creating a new link for device " + deviceInfo.id);
|
||||
link = new LanLink(context, deviceInfo, this, socket);
|
||||
visibleDevices.put(deviceInfo.id, link);
|
||||
onConnectionReceived(link);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +379,8 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkPacket identity = NetworkPacket.createIdentityPacket(context);
|
||||
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
|
||||
NetworkPacket identity = myDeviceInfo.toIdentityPacket();
|
||||
identity.set("tcpPort", tcpServer.getLocalPort());
|
||||
|
||||
byte[] bytes;
|
||||
|
@@ -14,12 +14,14 @@ import androidx.annotation.WorkerThread;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceInfo;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
|
||||
public class LoopbackLink extends BaseLink {
|
||||
|
||||
public LoopbackLink(Context context, BaseLinkProvider linkProvider) {
|
||||
super(context, "loopback", linkProvider);
|
||||
super(context, linkProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -40,4 +42,9 @@ public class LoopbackLink extends BaseLink {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceInfo getDeviceInfo() {
|
||||
return DeviceHelper.getDeviceInfo(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -9,9 +9,6 @@ package org.kde.kdeconnect.Backends.LoopbackBackend;
|
||||
import android.content.Context;
|
||||
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
|
||||
public class LoopbackLinkProvider extends BaseLinkProvider {
|
||||
|
||||
@@ -32,9 +29,8 @@ public class LoopbackLinkProvider extends BaseLinkProvider {
|
||||
|
||||
@Override
|
||||
public void onNetworkChange() {
|
||||
NetworkPacket np = NetworkPacket.createIdentityPacket(context);
|
||||
String deviceId = DeviceHelper.getDeviceId(context);
|
||||
onConnectionReceived(deviceId, SslHelper.certificate, np, new LoopbackLink(context, this));
|
||||
LoopbackLink link = new LoopbackLink(context, this);
|
||||
onConnectionReceived(link);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -206,6 +206,7 @@ public class BackgroundService extends Service {
|
||||
.setContentIntent(pi)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN) //MIN so it's not shown in the status bar before Oreo, on Oreo it will be bumped to LOW
|
||||
.setShowWhen(false)
|
||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
||||
.setAutoCancel(false);
|
||||
notification.setGroup("BackgroundService");
|
||||
|
||||
|
@@ -14,7 +14,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
@@ -26,7 +25,6 @@ import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.apache.commons.collections4.MultiValuedMap;
|
||||
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Helpers.NotificationHelper;
|
||||
@@ -38,13 +36,8 @@ import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
@@ -53,30 +46,26 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final String deviceId;
|
||||
private String name;
|
||||
public Certificate certificate;
|
||||
final DeviceInfo deviceInfo;
|
||||
|
||||
private int notificationId;
|
||||
private int protocolVersion;
|
||||
private DeviceType deviceType;
|
||||
PairingHandler pairingHandler;
|
||||
private final CopyOnWriteArrayList<PairingHandler.PairingCallback> pairingCallbacks = new CopyOnWriteArrayList<>();
|
||||
private final CopyOnWriteArrayList<BaseLink> links = new CopyOnWriteArrayList<>();
|
||||
private DevicePacketQueue packetQueue;
|
||||
private List<String> supportedPlugins = new ArrayList<>();
|
||||
private List<String> supportedPlugins;
|
||||
private final ConcurrentHashMap<String, Plugin> plugins = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Plugin> pluginsWithoutPermissions = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Plugin> pluginsWithoutOptionalPermissions = new ConcurrentHashMap<>();
|
||||
private MultiValuedMap<String, String> pluginsByIncomingInterface = new ArrayListValuedHashMap<>();
|
||||
private final SharedPreferences settings;
|
||||
private final CopyOnWriteArrayList<PluginsChangedListener> pluginsChangedListeners = new CopyOnWriteArrayList<>();
|
||||
private Set<String> incomingCapabilities = new HashSet<>();
|
||||
|
||||
public boolean supportsPacketType(String type) {
|
||||
if (incomingCapabilities == null) {
|
||||
if (deviceInfo.incomingCapabilities == null) {
|
||||
return true;
|
||||
} else {
|
||||
return incomingCapabilities.contains(type);
|
||||
return deviceInfo.incomingCapabilities.contains(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,109 +73,52 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
void onPluginsChanged(@NonNull Device device);
|
||||
}
|
||||
|
||||
public enum DeviceType {
|
||||
Phone,
|
||||
Tablet,
|
||||
Computer,
|
||||
Tv;
|
||||
|
||||
static public DeviceType FromString(String s) {
|
||||
if ("tablet".equals(s)) return Tablet;
|
||||
if ("phone".equals(s)) return Phone;
|
||||
if ("tv".equals(s)) return Tv;
|
||||
return Computer; //Default
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case Tablet:
|
||||
return "tablet";
|
||||
case Phone:
|
||||
return "phone";
|
||||
case Tv:
|
||||
return "tv";
|
||||
default:
|
||||
return "desktop";
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable getIcon(Context context) {
|
||||
int drawableId;
|
||||
switch (this) {
|
||||
case Phone:
|
||||
drawableId = R.drawable.ic_device_phone_32dp;
|
||||
break;
|
||||
case Tablet:
|
||||
drawableId = R.drawable.ic_device_tablet_32dp;
|
||||
break;
|
||||
case Tv:
|
||||
drawableId = R.drawable.ic_device_tv_32dp;
|
||||
break;
|
||||
default:
|
||||
drawableId = R.drawable.ic_device_laptop_32dp;
|
||||
}
|
||||
return ContextCompat.getDrawable(context, drawableId);
|
||||
}
|
||||
}
|
||||
|
||||
// Remembered trusted device, we need to wait for a incoming Link to communicate
|
||||
Device(@NonNull Context context, @NonNull String deviceId) throws CertificateException {
|
||||
|
||||
/**
|
||||
* Constructor for remembered, already-trusted devices.
|
||||
* Given the deviceId, it will load the other properties from SharedPreferences.
|
||||
*/
|
||||
Device(@NonNull Context context, @NonNull String deviceId) {
|
||||
this.context = context;
|
||||
|
||||
this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
|
||||
this.deviceInfo = DeviceInfo.loadFromSettings(context, deviceId, settings);
|
||||
this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.Paired);
|
||||
|
||||
this.deviceId = deviceId;
|
||||
this.name = settings.getString("deviceName", context.getString(R.string.unknown_device));
|
||||
this.protocolVersion = 0; //We don't know it yet
|
||||
this.deviceType = DeviceType.FromString(settings.getString("deviceType", "desktop"));
|
||||
this.certificate = SslHelper.getDeviceCertificate(context, deviceId);
|
||||
|
||||
Log.i("Device","Loading trusted device: " + this.name);
|
||||
|
||||
//Assume every plugin is supported until addLink is called and we can get the actual list
|
||||
supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins());
|
||||
|
||||
//Do not load plugins yet, the device is not present
|
||||
//reloadPluginsFromSettings();
|
||||
this.supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); // Assume all are supported until we receive capabilities
|
||||
Log.i("Device","Loading trusted device: " + deviceInfo.name);
|
||||
}
|
||||
|
||||
// Device known via an incoming connection sent to us via a Link, we don't trust it yet
|
||||
Device(@NonNull Context context, @NonNull String deviceId, @NonNull Certificate certificate, @NonNull NetworkPacket identityPacket, @NonNull BaseLink dl) {
|
||||
Log.i("Device","Creating untrusted device");
|
||||
|
||||
/**
|
||||
* Constructor for devices discovered but not trusted yet.
|
||||
* Gets the DeviceInfo by calling link.getDeviceInfo() on the link passed.
|
||||
* This constructor also calls addLink() with the link you pass to it, since it's not legal to have an unpaired Device with 0 links.
|
||||
*/
|
||||
Device(@NonNull Context context, @NonNull BaseLink link) {
|
||||
this.context = context;
|
||||
|
||||
this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
|
||||
this.deviceInfo = link.getDeviceInfo();
|
||||
this.settings = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE);
|
||||
this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.NotPaired);
|
||||
|
||||
this.deviceId = deviceId;
|
||||
this.certificate = certificate;
|
||||
|
||||
// The following properties are read from the identityPacket in addLink since they can change in future identity packets
|
||||
this.name = context.getString(R.string.unknown_device);
|
||||
this.deviceType = DeviceType.Computer;
|
||||
this.protocolVersion = 0;
|
||||
|
||||
addLink(identityPacket, dl);
|
||||
this.supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); // Assume all are supported until we receive capabilities
|
||||
Log.i("Device","Creating untrusted device: "+ deviceInfo.name);
|
||||
addLink(link);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return StringUtils.defaultString(name, context.getString(R.string.unknown_device));
|
||||
return deviceInfo.name;
|
||||
}
|
||||
|
||||
public Drawable getIcon() {
|
||||
return deviceType.getIcon(context);
|
||||
return deviceInfo.type.getIcon(context);
|
||||
}
|
||||
|
||||
public DeviceType getDeviceType() {
|
||||
return deviceType;
|
||||
return deviceInfo.type;
|
||||
}
|
||||
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
return deviceInfo.id;
|
||||
}
|
||||
|
||||
public Certificate getCertificate() {
|
||||
return deviceInfo.certificate;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
@@ -195,7 +127,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
|
||||
//Returns 0 if the version matches, < 0 if it is older or > 0 if it is newer
|
||||
public int compareProtocolVersion() {
|
||||
return protocolVersion - DeviceHelper.ProtocolVersion;
|
||||
return deviceInfo.protocolVersion - DeviceHelper.ProtocolVersion;
|
||||
}
|
||||
|
||||
|
||||
@@ -254,28 +186,26 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
|
||||
@Override
|
||||
public void pairingSuccessful() {
|
||||
Log.i("Device", "pairing successful, adding to trusted devices list");
|
||||
|
||||
hidePairingNotification();
|
||||
|
||||
// Store current device certificate so we can check it in the future (TOFU)
|
||||
SharedPreferences.Editor editor = context.getSharedPreferences(getDeviceId(), Context.MODE_PRIVATE).edit();
|
||||
try {
|
||||
String encodedCertificate = Base64.encodeToString(certificate.getEncoded(), 0);
|
||||
editor.putString("certificate", encodedCertificate);
|
||||
} catch(CertificateEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
editor.putString("deviceName", name);
|
||||
editor.putString("deviceType", deviceType.toString());
|
||||
editor.apply();
|
||||
deviceInfo.saveInSettings(Device.this.settings);
|
||||
|
||||
// Store as trusted device
|
||||
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
|
||||
preferences.edit().putBoolean(deviceId, true).apply();
|
||||
preferences.edit().putBoolean(deviceInfo.id, true).apply();
|
||||
|
||||
reloadPluginsFromSettings();
|
||||
try {
|
||||
reloadPluginsFromSettings();
|
||||
|
||||
for (PairingHandler.PairingCallback cb : pairingCallbacks) {
|
||||
cb.pairingSuccessful();
|
||||
for (PairingHandler.PairingCallback cb : pairingCallbacks) {
|
||||
cb.pairingSuccessful();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("PairingHandler", "Exception in pairingSuccessful. Not unpairing because saving the trusted device succeeded");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,10 +219,11 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
|
||||
@Override
|
||||
public void unpaired() {
|
||||
Log.i("Device", "unpaired, removing from trusted devices list");
|
||||
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
|
||||
preferences.edit().remove(deviceId).apply();
|
||||
preferences.edit().remove(deviceInfo.id).apply();
|
||||
|
||||
SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
|
||||
SharedPreferences devicePreferences = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE);
|
||||
devicePreferences.edit().clear().apply();
|
||||
|
||||
for (PairingHandler.PairingCallback cb : pairingCallbacks) {
|
||||
@@ -334,7 +265,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
|
||||
final NotificationManager notificationManager = ContextCompat.getSystemService(getContext(), NotificationManager.class);
|
||||
|
||||
String verificationKeyShort = SslHelper.getVerificationKey(SslHelper.certificate, certificate).substring(8);
|
||||
String verificationKeyShort = SslHelper.getVerificationKey(SslHelper.certificate, deviceInfo.certificate).substring(8);
|
||||
|
||||
Notification noti = new NotificationCompat.Builder(getContext(), NotificationHelper.Channels.DEFAULT)
|
||||
.setContentTitle(res.getString(R.string.pairing_request_from, getName()))
|
||||
@@ -365,7 +296,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
return !links.isEmpty();
|
||||
}
|
||||
|
||||
public void addLink(NetworkPacket identityPacket, BaseLink link) {
|
||||
public void addLink(BaseLink link) {
|
||||
if (links.isEmpty()) {
|
||||
packetQueue = new DevicePacketQueue(this);
|
||||
}
|
||||
@@ -373,33 +304,11 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
links.add(link);
|
||||
link.addPacketReceiver(this);
|
||||
|
||||
this.protocolVersion = identityPacket.getInt("protocolVersion");
|
||||
boolean hasChanges = updateDeviceInfo(link.getDeviceInfo());
|
||||
|
||||
if (identityPacket.has("deviceName")) {
|
||||
this.name = identityPacket.getString("deviceName", this.name);
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putString("deviceName", this.name);
|
||||
editor.apply();
|
||||
if (hasChanges || links.size() == 1) {
|
||||
reloadPluginsFromSettings();
|
||||
}
|
||||
|
||||
if (identityPacket.has("deviceType")) {
|
||||
this.deviceType = DeviceType.FromString(identityPacket.getString("deviceType", "desktop"));
|
||||
}
|
||||
|
||||
Log.i("KDE/Device", "addLink " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size());
|
||||
|
||||
Set<String> outgoingCapabilities = identityPacket.getStringSet("outgoingCapabilities", null);
|
||||
Set<String> incomingCapabilities = identityPacket.getStringSet("incomingCapabilities", null);
|
||||
|
||||
if (incomingCapabilities != null && outgoingCapabilities != null) {
|
||||
supportedPlugins = new Vector<>(PluginFactory.pluginsForCapabilities(incomingCapabilities, outgoingCapabilities));
|
||||
} else {
|
||||
supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins());
|
||||
}
|
||||
this.incomingCapabilities = incomingCapabilities;
|
||||
|
||||
reloadPluginsFromSettings();
|
||||
|
||||
}
|
||||
|
||||
public void removeLink(BaseLink link) {
|
||||
@@ -417,6 +326,30 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean updateDeviceInfo(@NonNull DeviceInfo newDeviceInfo) {
|
||||
|
||||
boolean hasChanges = false;
|
||||
if (!deviceInfo.name.equals(newDeviceInfo.name) || deviceInfo.type != newDeviceInfo.type) {
|
||||
hasChanges = true;
|
||||
deviceInfo.name = newDeviceInfo.name;
|
||||
deviceInfo.type = newDeviceInfo.type;
|
||||
if (isPaired()) {
|
||||
deviceInfo.saveInSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceInfo.outgoingCapabilities != newDeviceInfo.outgoingCapabilities ||
|
||||
deviceInfo.incomingCapabilities != newDeviceInfo.incomingCapabilities) {
|
||||
if (newDeviceInfo.outgoingCapabilities != null && newDeviceInfo.incomingCapabilities != null) {
|
||||
hasChanges = true;
|
||||
Log.i("updateDeviceInfo", "Updating supported plugins according to new capabilities");
|
||||
supportedPlugins = new Vector<>(PluginFactory.pluginsForCapabilities(newDeviceInfo.incomingCapabilities, newDeviceInfo.outgoingCapabilities));
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceived(@NonNull NetworkPacket np) {
|
||||
|
||||
@@ -580,7 +513,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Log.e("KDE/sendPacket", "No device link (of " + links.size() + " available) could send the packet. Packet " + np.getType() + " to " + name + " lost!");
|
||||
Log.e("KDE/sendPacket", "No device link (of " + links.size() + " available) could send the packet. Packet " + np.getType() + " to " + deviceInfo.name + " lost!");
|
||||
}
|
||||
|
||||
return success;
|
||||
@@ -641,8 +574,6 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
return false;
|
||||
}
|
||||
|
||||
plugins.put(pluginKey, plugin);
|
||||
|
||||
if (!plugin.checkRequiredPermissions()) {
|
||||
Log.d("KDE/addPlugin", "No permission " + pluginKey);
|
||||
plugins.remove(pluginKey);
|
||||
@@ -650,6 +581,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
return false;
|
||||
} else {
|
||||
Log.d("KDE/addPlugin", "Permissions OK " + pluginKey);
|
||||
plugins.put(pluginKey, plugin);
|
||||
pluginsWithoutPermissions.remove(pluginKey);
|
||||
if (plugin.checkOptionalPermissions()) {
|
||||
Log.d("KDE/addPlugin", "Optional Permissions OK " + pluginKey);
|
||||
@@ -697,6 +629,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
}
|
||||
|
||||
public void reloadPluginsFromSettings() {
|
||||
Log.i("Device", deviceInfo.name +": reloading plugins");
|
||||
MultiValuedMap<String, String> newPluginsByIncomingInterface = new ArrayListValuedHashMap<>();
|
||||
|
||||
for (String pluginKey : supportedPlugins) {
|
||||
|
133
src/org/kde/kdeconnect/DeviceInfo.kt
Normal file
133
src/org/kde/kdeconnect/DeviceInfo.kt
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Base64
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
|
||||
import org.kde.kdeconnect_tp.R
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateEncodingException
|
||||
|
||||
/**
|
||||
* DeviceInfo contains all the properties needed to instantiate a Device.
|
||||
*/
|
||||
class DeviceInfo(
|
||||
@JvmField val id : String,
|
||||
@JvmField val certificate : Certificate,
|
||||
@JvmField var name : String,
|
||||
@JvmField var type : DeviceType,
|
||||
@JvmField var protocolVersion : Int = 0,
|
||||
@JvmField var incomingCapabilities : Set<String>? = null,
|
||||
@JvmField var outgoingCapabilities : Set<String>? = null,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Saves the info in settings so it can be restored later using loadFromSettings().
|
||||
* This is used to keep info from paired devices, even when they are not reachable.
|
||||
* The capabilities and protocol version are not persisted.
|
||||
*/
|
||||
fun saveInSettings(settings: SharedPreferences) {
|
||||
val editor = settings.edit()
|
||||
try {
|
||||
val encodedCertificate = Base64.encodeToString(certificate.encoded, 0)
|
||||
editor.putString("certificate", encodedCertificate)
|
||||
} catch (e: CertificateEncodingException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
editor.putString("deviceName", name)
|
||||
editor.putString("deviceType", type.toString())
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Serializes to a NetworkPacket, which LanLinkProvider uses to send this data over the network.
|
||||
* The serialization doesn't include the certificate, since LanLink can query that from the socket.
|
||||
* Can be deserialized using fromIdentityPacketAndCert(), given a certificate.
|
||||
*/
|
||||
fun toIdentityPacket(): NetworkPacket {
|
||||
val np = NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY)
|
||||
np.set("deviceId", id)
|
||||
np.set("deviceName", name)
|
||||
np.set("protocolVersion", protocolVersion)
|
||||
np.set("deviceType", type.toString())
|
||||
np.set("incomingCapabilities", incomingCapabilities)
|
||||
np.set("outgoingCapabilities", outgoingCapabilities)
|
||||
return np
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Recreates a DeviceInfo object that was persisted using saveInSettings()
|
||||
*/
|
||||
@JvmStatic
|
||||
fun loadFromSettings(context : Context, deviceId: String, settings: SharedPreferences): DeviceInfo {
|
||||
val deviceName = settings.getString("deviceName", "unknown")!!
|
||||
val deviceType = DeviceType.fromString(settings.getString("deviceType", "desktop")!!)
|
||||
val certificate = SslHelper.getDeviceCertificate(context, deviceId)
|
||||
return DeviceInfo(id = deviceId, name = deviceName, type = deviceType, certificate = certificate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreates a DeviceInfo object that was serialized using toIdentityPacket().
|
||||
* Since toIdentityPacket() doesn't serialize the certificate, this needs to be passed separately.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromIdentityPacketAndCert(identityPacket : NetworkPacket, certificate : Certificate): DeviceInfo {
|
||||
val deviceId = identityPacket.getString("deviceId")
|
||||
val deviceName = identityPacket.getString("deviceName", "unknown")
|
||||
val protocolVersion = identityPacket.getInt("protocolVersion")
|
||||
val deviceType = DeviceType.fromString(identityPacket.getString("deviceType", "desktop"))
|
||||
val incomingCapabilities = identityPacket.getStringSet("incomingCapabilities")
|
||||
val outgoingCapabilities = identityPacket.getStringSet("outgoingCapabilities")
|
||||
return DeviceInfo(id = deviceId, name = deviceName, type = deviceType, certificate = certificate,
|
||||
protocolVersion = protocolVersion, incomingCapabilities = incomingCapabilities, outgoingCapabilities = outgoingCapabilities)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
enum class DeviceType {
|
||||
Phone, Tablet, Computer, Tv;
|
||||
|
||||
override fun toString(): String {
|
||||
return when (this) {
|
||||
Tablet -> "tablet"
|
||||
Phone -> "phone"
|
||||
Tv -> "tv"
|
||||
else -> "desktop"
|
||||
}
|
||||
}
|
||||
|
||||
fun getIcon(context: Context): Drawable? {
|
||||
val drawableId: Int = when (this) {
|
||||
Phone -> R.drawable.ic_device_phone_32dp
|
||||
Tablet -> R.drawable.ic_device_tablet_32dp
|
||||
Tv -> R.drawable.ic_device_tv_32dp
|
||||
else -> R.drawable.ic_device_laptop_32dp
|
||||
}
|
||||
return ContextCompat.getDrawable(context, drawableId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromString(s: String): DeviceType {
|
||||
return when (s) {
|
||||
"phone" -> Phone
|
||||
"tablet" -> Tablet
|
||||
"tv" -> Tv
|
||||
else -> Computer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,10 +16,14 @@ import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import com.univocity.parsers.common.TextParsingException;
|
||||
import com.univocity.parsers.csv.CsvParser;
|
||||
import com.univocity.parsers.csv.CsvParserSettings;
|
||||
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceInfo;
|
||||
import org.kde.kdeconnect.DeviceType;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@@ -53,13 +57,13 @@ public class DeviceHelper {
|
||||
return (uiMode & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION;
|
||||
}
|
||||
|
||||
public static Device.DeviceType getDeviceType(Context context) {
|
||||
public static DeviceType getDeviceType(Context context) {
|
||||
if (isTv(context)) {
|
||||
return Device.DeviceType.Tv;
|
||||
return DeviceType.Tv;
|
||||
} else if (isTablet()) {
|
||||
return Device.DeviceType.Tablet;
|
||||
return DeviceType.Tablet;
|
||||
} else {
|
||||
return Device.DeviceType.Phone;
|
||||
return DeviceType.Phone;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +112,7 @@ public class DeviceHelper {
|
||||
Log.e("DeviceHelper", "Didn't find a device name for " + Build.MODEL);
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
} catch(IOException | TextParsingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
fetchingName = false;
|
||||
@@ -146,4 +150,14 @@ public class DeviceHelper {
|
||||
return preferences.getString(KEY_DEVICE_ID_PREFERENCE, null);
|
||||
}
|
||||
|
||||
public static DeviceInfo getDeviceInfo(Context context) {
|
||||
return new DeviceInfo(getDeviceId(context),
|
||||
SslHelper.certificate,
|
||||
getDeviceName(context),
|
||||
DeviceHelper.getDeviceType(context),
|
||||
ProtocolVersion,
|
||||
PluginFactory.getIncomingCapabilities(),
|
||||
PluginFactory.getOutgoingCapabilities());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
* SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect;
|
||||
|
||||
@@ -24,8 +24,6 @@ import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -115,14 +113,9 @@ public class KdeConnect extends Application {
|
||||
for (String deviceId : trustedDevices) {
|
||||
//Log.e("BackgroundService", "Loading device "+deviceId);
|
||||
if (preferences.getBoolean(deviceId, false)) {
|
||||
try {
|
||||
Device device = new Device(this, deviceId);
|
||||
devices.put(deviceId, device);
|
||||
device.addPairingCallback(devicePairingCallback);
|
||||
} catch (CertificateException e) {
|
||||
Log.e("KdeConnect", "Could not load trusted device, certificate not valid: " + deviceId);
|
||||
e.printStackTrace();
|
||||
}
|
||||
Device device = new Device(this, deviceId);
|
||||
devices.put(deviceId, device);
|
||||
device.addPairingCallback(devicePairingCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,18 +144,13 @@ public class KdeConnect extends Application {
|
||||
|
||||
private final BaseLinkProvider.ConnectionReceiver connectionListener = new BaseLinkProvider.ConnectionReceiver() {
|
||||
@Override
|
||||
public void onConnectionReceived(@NonNull final String deviceId,
|
||||
@NonNull final Certificate certificate,
|
||||
@NonNull final NetworkPacket identityPacket,
|
||||
@NonNull final BaseLink link) {
|
||||
Device device = devices.get(deviceId);
|
||||
public void onConnectionReceived(@NonNull final BaseLink link) {
|
||||
Device device = devices.get(link.getDeviceId());
|
||||
if (device != null) {
|
||||
Log.i("KDE/Application", "addLink, known device: " + deviceId);
|
||||
device.addLink(identityPacket, link);
|
||||
device.addLink(link);
|
||||
} else {
|
||||
Log.i("KDE/Application", "addLink,unknown device: " + deviceId);
|
||||
device = new Device(KdeConnect.this, deviceId, certificate, identityPacket, link);
|
||||
devices.put(deviceId, device);
|
||||
device = new Device(KdeConnect.this, link);
|
||||
devices.put(link.getDeviceId(), device);
|
||||
device.addPairingCallback(devicePairingCallback);
|
||||
}
|
||||
onDeviceListChanged();
|
||||
|
@@ -26,14 +26,6 @@ public class KdeConnectBroadcastReceiver extends BroadcastReceiver {
|
||||
Log.i("KdeConnect", "MyUpdateReceiver");
|
||||
BackgroundService.Start(context);
|
||||
break;
|
||||
case Intent.ACTION_PACKAGE_REPLACED:
|
||||
Log.i("KdeConnect", "UpdateReceiver");
|
||||
if (!intent.getData().getSchemeSpecificPart().equals(context.getPackageName())) {
|
||||
Log.i("KdeConnect", "Ignoring, it's not me!");
|
||||
return;
|
||||
}
|
||||
BackgroundService.Start(context);
|
||||
break;
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
Log.i("KdeConnect", "KdeConnectBroadcastReceiver");
|
||||
BackgroundService.Start(context);
|
||||
|
@@ -6,15 +6,10 @@
|
||||
|
||||
package org.kde.kdeconnect;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -263,26 +258,6 @@ public class NetworkPacket {
|
||||
return np;
|
||||
}
|
||||
|
||||
static public NetworkPacket createIdentityPacket(Context context) {
|
||||
|
||||
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY);
|
||||
|
||||
String deviceId = DeviceHelper.getDeviceId(context);
|
||||
try {
|
||||
np.mBody.put("deviceId", deviceId);
|
||||
np.mBody.put("deviceName", DeviceHelper.getDeviceName(context));
|
||||
np.mBody.put("protocolVersion", DeviceHelper.ProtocolVersion);
|
||||
np.mBody.put("deviceType", DeviceHelper.getDeviceType(context).toString());
|
||||
np.mBody.put("incomingCapabilities", new JSONArray(PluginFactory.getIncomingCapabilities()));
|
||||
np.mBody.put("outgoingCapabilities", new JSONArray(PluginFactory.getOutgoingCapabilities()));
|
||||
} catch (Exception e) {
|
||||
Log.e("NetworkPacket", "Exception on createIdentityPacket", e);
|
||||
}
|
||||
|
||||
return np;
|
||||
|
||||
}
|
||||
|
||||
public void setPayload(Payload payload) { mPayload = payload; }
|
||||
|
||||
public Payload getPayload() {
|
||||
|
@@ -19,7 +19,7 @@ import android.view.KeyEvent;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceType;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
@@ -33,7 +33,7 @@ public class BigscreenPlugin extends Plugin {
|
||||
|
||||
@Override
|
||||
public boolean isCompatible() {
|
||||
return device.getDeviceType().equals(Device.DeviceType.Tv) && super.isCompatible();
|
||||
return device.getDeviceType().equals(DeviceType.Tv) && super.isCompatible();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -219,12 +219,17 @@ public class ConnectivityReportPlugin extends Plugin {
|
||||
runWithLooper(() -> {
|
||||
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
TelephonyHelper.cancelActiveSubscriptionIDsListener(context, subListener);
|
||||
if (subListener != null) {
|
||||
TelephonyHelper.cancelActiveSubscriptionIDsListener(context, subListener);
|
||||
subListener = null;
|
||||
}
|
||||
}
|
||||
for (Integer subID : listeners.keySet()) {
|
||||
Log.i("ConnectivityReport", "Removed subscription ID " + subID);
|
||||
tm.listen(listeners.get(subID), PhoneStateListener.LISTEN_NONE);
|
||||
}
|
||||
listeners.clear();
|
||||
states.clear();
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -204,10 +204,12 @@ internal object AlbumArtCache {
|
||||
}
|
||||
|
||||
//Only fetch an URL if we're not fetching it already
|
||||
if (url in fetchUrlList || url in isFetchingList) {
|
||||
return
|
||||
synchronized(fetchUrlList) {
|
||||
if (url in fetchUrlList || url in isFetchingList) {
|
||||
return
|
||||
}
|
||||
fetchUrlList.add(url)
|
||||
}
|
||||
fetchUrlList.add(url)
|
||||
initiateFetch()
|
||||
}
|
||||
|
||||
@@ -215,12 +217,14 @@ internal object AlbumArtCache {
|
||||
* Does the actual fetching and makes sure only not too many fetches are running at the same time
|
||||
*/
|
||||
private fun initiateFetch() {
|
||||
if (numFetching >= 2 || fetchUrlList.isEmpty()) return
|
||||
|
||||
//Fetch the last-requested url first, it will probably be needed first
|
||||
val url = fetchUrlList.last()
|
||||
//Remove the url from the to-fetch list
|
||||
fetchUrlList.remove(url)
|
||||
var url : URL;
|
||||
synchronized(fetchUrlList) {
|
||||
if (numFetching >= 2 || fetchUrlList.isEmpty()) return
|
||||
//Fetch the last-requested url first, it will probably be needed first
|
||||
url = fetchUrlList.last()
|
||||
//Remove the url from the to-fetch list
|
||||
fetchUrlList.remove(url)
|
||||
}
|
||||
if ("file" == url.protocol) {
|
||||
throw AssertionError("Not file urls should be possible here!")
|
||||
}
|
||||
|
@@ -168,12 +168,13 @@ public class MprisMediaSession implements
|
||||
* Prefers playing devices/mpris players, but tries to keep displaying the same
|
||||
* player and device, while possible.
|
||||
*/
|
||||
private void updateCurrentPlayer() {
|
||||
private MprisPlugin.MprisPlayer updateCurrentPlayer() {
|
||||
Pair<Device, MprisPlugin.MprisPlayer> player = findPlayer();
|
||||
|
||||
//Update the last-displayed device and player
|
||||
notificationDevice = player.first == null ? null : player.first.getDeviceId();
|
||||
notificationPlayer = player.second;
|
||||
return notificationPlayer;
|
||||
}
|
||||
|
||||
private Pair<Device, MprisPlugin.MprisPlayer> findPlayer() {
|
||||
@@ -215,7 +216,7 @@ public class MprisMediaSession implements
|
||||
}
|
||||
|
||||
private MprisPlugin.MprisPlayer getPlayerFromDevice(Device device, MprisPlugin.MprisPlayer preferredPlayer) {
|
||||
if (!mprisDevices.contains(device.getDeviceId()))
|
||||
if (device == null || !mprisDevices.contains(device.getDeviceId()))
|
||||
return null;
|
||||
|
||||
MprisPlugin plugin = device.getPlugin(MprisPlugin.class);
|
||||
@@ -263,6 +264,21 @@ public class MprisMediaSession implements
|
||||
return;
|
||||
}
|
||||
|
||||
//Make sure our information is up-to-date
|
||||
MprisPlugin.MprisPlayer currentPlayer = updateCurrentPlayer();
|
||||
|
||||
Device device = KdeConnect.getInstance().getDevice(notificationDevice);
|
||||
if (device == null) {
|
||||
closeMediaNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
//If the player disappeared (and no other playing one found), just remove the notification
|
||||
if (currentPlayer == null) {
|
||||
closeMediaNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (instance) {
|
||||
if (mediaSession == null) {
|
||||
mediaSession = new MediaSessionCompat(context, MPRIS_MEDIA_SESSION_TAG);
|
||||
@@ -272,33 +288,24 @@ public class MprisMediaSession implements
|
||||
}
|
||||
}
|
||||
|
||||
//Make sure our information is up-to-date
|
||||
updateCurrentPlayer();
|
||||
|
||||
//If the player disappeared (and no other playing one found), just remove the notification
|
||||
if (notificationPlayer == null) {
|
||||
closeMediaNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
updateRemoteDeviceVolumeControl();
|
||||
|
||||
MediaMetadataCompat.Builder metadata = new MediaMetadataCompat.Builder();
|
||||
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getTitle());
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, currentPlayer.getTitle());
|
||||
|
||||
if (!notificationPlayer.getArtist().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, notificationPlayer.getArtist());
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, notificationPlayer.getArtist());
|
||||
if (!currentPlayer.getArtist().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, currentPlayer.getArtist());
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentPlayer.getArtist());
|
||||
}
|
||||
if (!notificationPlayer.getAlbum().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, notificationPlayer.getAlbum());
|
||||
if (!currentPlayer.getAlbum().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, currentPlayer.getAlbum());
|
||||
}
|
||||
if (notificationPlayer.getLength() > 0) {
|
||||
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength());
|
||||
if (currentPlayer.getLength() > 0) {
|
||||
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, currentPlayer.getLength());
|
||||
}
|
||||
|
||||
Bitmap albumArt = notificationPlayer.getAlbumArt();
|
||||
Bitmap albumArt = currentPlayer.getAlbumArt();
|
||||
if (albumArt != null) {
|
||||
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt);
|
||||
}
|
||||
@@ -306,17 +313,17 @@ public class MprisMediaSession implements
|
||||
mediaSession.setMetadata(metadata.build());
|
||||
PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder();
|
||||
|
||||
if (notificationPlayer.isPlaying()) {
|
||||
playbackState.setState(PlaybackStateCompat.STATE_PLAYING, notificationPlayer.getPosition(), 1.0f);
|
||||
if (currentPlayer.isPlaying()) {
|
||||
playbackState.setState(PlaybackStateCompat.STATE_PLAYING, currentPlayer.getPosition(), 1.0f);
|
||||
} else {
|
||||
playbackState.setState(PlaybackStateCompat.STATE_PAUSED, notificationPlayer.getPosition(), 0.0f);
|
||||
playbackState.setState(PlaybackStateCompat.STATE_PAUSED, currentPlayer.getPosition(), 0.0f);
|
||||
}
|
||||
|
||||
//Create all actions (previous/play/pause/next)
|
||||
Intent iPlay = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iPlay.setAction(MprisMediaNotificationReceiver.ACTION_PLAY);
|
||||
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.getPlayerName());
|
||||
PendingIntent piPlay = PendingIntent.getBroadcast(context, 0, iPlay, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aPlay = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_play_white, context.getString(R.string.mpris_play), piPlay);
|
||||
@@ -324,7 +331,7 @@ public class MprisMediaSession implements
|
||||
Intent iPause = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iPause.setAction(MprisMediaNotificationReceiver.ACTION_PAUSE);
|
||||
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.getPlayerName());
|
||||
PendingIntent piPause = PendingIntent.getBroadcast(context, 0, iPause, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aPause = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_pause_white, context.getString(R.string.mpris_pause), piPause);
|
||||
@@ -332,7 +339,7 @@ public class MprisMediaSession implements
|
||||
Intent iPrevious = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iPrevious.setAction(MprisMediaNotificationReceiver.ACTION_PREVIOUS);
|
||||
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.getPlayerName());
|
||||
PendingIntent piPrevious = PendingIntent.getBroadcast(context, 0, iPrevious, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aPrevious = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_previous_white, context.getString(R.string.mpris_previous), piPrevious);
|
||||
@@ -340,14 +347,14 @@ public class MprisMediaSession implements
|
||||
Intent iNext = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iNext.setAction(MprisMediaNotificationReceiver.ACTION_NEXT);
|
||||
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.getPlayerName());
|
||||
PendingIntent piNext = PendingIntent.getBroadcast(context, 0, iNext, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aNext = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_next_white, context.getString(R.string.mpris_next), piNext);
|
||||
|
||||
Intent iOpenActivity = new Intent(context, MprisActivity.class);
|
||||
iOpenActivity.putExtra("deviceId", notificationDevice);
|
||||
iOpenActivity.putExtra("player", notificationPlayer.getPlayerName());
|
||||
iOpenActivity.putExtra("player", currentPlayer.getPlayerName());
|
||||
|
||||
PendingIntent piOpenActivity = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(iOpenActivity)
|
||||
@@ -362,30 +369,30 @@ public class MprisMediaSession implements
|
||||
.setShowWhen(false)
|
||||
.setColor(ContextCompat.getColor(context, R.color.primary))
|
||||
.setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setSubText(KdeConnect.getInstance().getDevice(notificationDevice).getName());
|
||||
.setSubText(device.getName());
|
||||
|
||||
notification.setContentTitle(notificationPlayer.getTitle());
|
||||
notification.setContentTitle(currentPlayer.getTitle());
|
||||
|
||||
//Only set the notification body text if we have an author and/or album
|
||||
if (!notificationPlayer.getArtist().isEmpty() && !notificationPlayer.getAlbum().isEmpty()) {
|
||||
notification.setContentText(notificationPlayer.getArtist() + " - " + notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayerName() + ")");
|
||||
} else if (!notificationPlayer.getArtist().isEmpty()) {
|
||||
notification.setContentText(notificationPlayer.getArtist() + " (" + notificationPlayer.getPlayerName() + ")");
|
||||
} else if (!notificationPlayer.getAlbum().isEmpty()) {
|
||||
notification.setContentText(notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayerName() + ")");
|
||||
if (!currentPlayer.getArtist().isEmpty() && !currentPlayer.getAlbum().isEmpty()) {
|
||||
notification.setContentText(currentPlayer.getArtist() + " - " + currentPlayer.getAlbum() + " (" + currentPlayer.getPlayerName() + ")");
|
||||
} else if (!currentPlayer.getArtist().isEmpty()) {
|
||||
notification.setContentText(currentPlayer.getArtist() + " (" + currentPlayer.getPlayerName() + ")");
|
||||
} else if (!currentPlayer.getAlbum().isEmpty()) {
|
||||
notification.setContentText(currentPlayer.getAlbum() + " (" + currentPlayer.getPlayerName() + ")");
|
||||
} else {
|
||||
notification.setContentText(notificationPlayer.getPlayerName());
|
||||
notification.setContentText(currentPlayer.getPlayerName());
|
||||
}
|
||||
|
||||
if (albumArt != null) {
|
||||
notification.setLargeIcon(albumArt);
|
||||
}
|
||||
|
||||
if (!notificationPlayer.isPlaying()) {
|
||||
if (!currentPlayer.isPlaying()) {
|
||||
Intent iCloseNotification = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION);
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.getPlayerName());
|
||||
PendingIntent piCloseNotification = PendingIntent.getBroadcast(context, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
notification.setDeleteIntent(piCloseNotification);
|
||||
}
|
||||
@@ -393,37 +400,37 @@ public class MprisMediaSession implements
|
||||
//Add media control actions
|
||||
int numActions = 0;
|
||||
long playbackActions = 0;
|
||||
if (notificationPlayer.isGoPreviousAllowed()) {
|
||||
if (currentPlayer.isGoPreviousAllowed()) {
|
||||
notification.addAction(aPrevious.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
||||
++numActions;
|
||||
}
|
||||
if (notificationPlayer.isPlaying() && notificationPlayer.isPauseAllowed()) {
|
||||
if (currentPlayer.isPlaying() && currentPlayer.isPauseAllowed()) {
|
||||
notification.addAction(aPause.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
|
||||
++numActions;
|
||||
}
|
||||
if (!notificationPlayer.isPlaying() && notificationPlayer.isPlayAllowed()) {
|
||||
if (!currentPlayer.isPlaying() && currentPlayer.isPlayAllowed()) {
|
||||
notification.addAction(aPlay.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_PLAY;
|
||||
++numActions;
|
||||
}
|
||||
if (notificationPlayer.isGoNextAllowed()) {
|
||||
if (currentPlayer.isGoNextAllowed()) {
|
||||
notification.addAction(aNext.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
|
||||
++numActions;
|
||||
}
|
||||
// Documentation says that this was added in Lollipop (21) but it seems to cause crashes on < Pie (28)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
if (notificationPlayer.isSeekAllowed()) {
|
||||
if (currentPlayer.isSeekAllowed()) {
|
||||
playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
|
||||
}
|
||||
}
|
||||
playbackState.setActions(playbackActions);
|
||||
mediaSession.setPlaybackState(playbackState.build());
|
||||
|
||||
//Only allow deletion if no music is notificationPlayer
|
||||
notification.setOngoing(notificationPlayer.isPlaying());
|
||||
//Only allow deletion if no music is currentPlayer
|
||||
notification.setOngoing(currentPlayer.isPlaying());
|
||||
|
||||
//Use the MediaStyle notification, so it feels like other media players. That also allows adding actions
|
||||
MediaStyle mediaStyle = new MediaStyle();
|
||||
@@ -495,11 +502,15 @@ public class MprisMediaSession implements
|
||||
|
||||
@Override
|
||||
public void onListenerConnected(NotificationReceiver service) {
|
||||
for (StatusBarNotification n : service.getActiveNotifications()) {
|
||||
if ("com.spotify.music".equals(n.getPackageName())) {
|
||||
spotifyRunning = true;
|
||||
updateMediaNotification();
|
||||
try {
|
||||
for (StatusBarNotification n : service.getActiveNotifications()) {
|
||||
if ("com.spotify.music".equals(n.getPackageName())) {
|
||||
spotifyRunning = true;
|
||||
updateMediaNotification();
|
||||
}
|
||||
}
|
||||
} catch(SecurityException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -245,7 +245,10 @@ public class MprisNowPlayingFragment extends Fragment implements VolumeKeyListen
|
||||
return; //Player hasn't actually changed
|
||||
}
|
||||
targetPlayer = plugin.getPlayerStatus(player);
|
||||
targetPlayerName = targetPlayer.getPlayerName();
|
||||
if (targetPlayer != null) {
|
||||
targetPlayerName = targetPlayer.getPlayerName();
|
||||
}
|
||||
|
||||
updatePlayerStatus(plugin);
|
||||
|
||||
if (targetPlayer != null && targetPlayer.isPlaying()) {
|
||||
|
@@ -24,6 +24,10 @@ class MprisReceiverPlayer {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public MediaController getController() {
|
||||
return controller;
|
||||
}
|
||||
|
||||
boolean isPlaying() {
|
||||
PlaybackState state = controller.getPlaybackState();
|
||||
if (state == null) return false;
|
||||
|
@@ -33,6 +33,8 @@ import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@PluginFactory.LoadablePlugin
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||
@@ -43,6 +45,7 @@ public class MprisReceiverPlugin extends Plugin {
|
||||
private static final String TAG = "MprisReceiver";
|
||||
|
||||
private HashMap<String, MprisReceiverPlayer> players;
|
||||
private HashMap<String, MprisReceiverCallback> playerCbs;
|
||||
private MediaSessionChangeListener mediaSessionChangeListener;
|
||||
|
||||
@Override
|
||||
@@ -52,6 +55,7 @@ public class MprisReceiverPlugin extends Plugin {
|
||||
return false;
|
||||
|
||||
players = new HashMap<>();
|
||||
playerCbs = new HashMap<>();
|
||||
try {
|
||||
MediaSessionManager manager = ContextCompat.getSystemService(context, MediaSessionManager.class);
|
||||
if (null == manager)
|
||||
@@ -175,7 +179,10 @@ public class MprisReceiverPlugin extends Plugin {
|
||||
if (null == controllers) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (MprisReceiverPlayer p : players.values()) {
|
||||
p.getController().unregisterCallback(playerCbs.get(p.getName()));
|
||||
}
|
||||
playerCbs.clear();
|
||||
players.clear();
|
||||
|
||||
createPlayers(controllers);
|
||||
@@ -189,7 +196,9 @@ public class MprisReceiverPlugin extends Plugin {
|
||||
if (controller.getPackageName().equals(context.getPackageName())) return;
|
||||
|
||||
MprisReceiverPlayer player = new MprisReceiverPlayer(controller, AppsHelper.appNameLookup(context, controller.getPackageName()));
|
||||
controller.registerCallback(new MprisReceiverCallback(this, player), new Handler(Looper.getMainLooper()));
|
||||
MprisReceiverCallback cb = new MprisReceiverCallback(this, player);
|
||||
controller.registerCallback(cb, new Handler(Looper.getMainLooper()));
|
||||
playerCbs.put(player.getName(), cb);
|
||||
players.put(player.getName(), player);
|
||||
}
|
||||
|
||||
@@ -209,6 +218,9 @@ public class MprisReceiverPlugin extends Plugin {
|
||||
np.set("player", player.getName());
|
||||
np.set("title", player.getTitle());
|
||||
np.set("artist", player.getArtist());
|
||||
String nowPlaying = Stream.of(player.getArtist(), player.getTitle())
|
||||
.filter(StringUtils::isNotEmpty).collect(Collectors.joining(" - "));
|
||||
np.set("nowPlaying", nowPlaying); // GSConnect 50 (so, Ubuntu 22.04) needs this
|
||||
np.set("album", player.getAlbum());
|
||||
np.set("isPlaying", player.isPlaying());
|
||||
np.set("pos", player.getPosition());
|
||||
|
@@ -18,7 +18,7 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceType;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
@@ -41,7 +41,7 @@ public class PresenterPlugin extends Plugin {
|
||||
|
||||
@Override
|
||||
public boolean isCompatible() {
|
||||
return !device.getDeviceType().equals(Device.DeviceType.Phone) && super.isCompatible();
|
||||
return !device.getDeviceType().equals(DeviceType.Phone) && super.isCompatible();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -92,7 +92,7 @@ class SimpleSftpServer {
|
||||
sshd.setCommandFactory(new ScpCommandFactory());
|
||||
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystem.Factory()));
|
||||
|
||||
keyAuth.deviceKey = device.certificate.getPublicKey();
|
||||
keyAuth.deviceKey = device.getCertificate().getPublicKey();
|
||||
|
||||
sshd.setPublickeyAuthenticator(keyAuth);
|
||||
sshd.setPasswordAuthenticator(passwordAuth);
|
||||
|
@@ -84,7 +84,7 @@ public class DeviceTest {
|
||||
MockSharedPreference deviceSettings = new MockSharedPreference();
|
||||
SharedPreferences.Editor editor = deviceSettings.edit();
|
||||
editor.putString("deviceName", name);
|
||||
editor.putString("deviceType", Device.DeviceType.Phone.toString());
|
||||
editor.putString("deviceType", DeviceType.Phone.toString());
|
||||
editor.putString("certificate", encodedCertificate);
|
||||
editor.apply();
|
||||
Mockito.when(context.getSharedPreferences(eq(deviceId), eq(Context.MODE_PRIVATE))).thenReturn(deviceSettings);
|
||||
@@ -110,12 +110,11 @@ public class DeviceTest {
|
||||
|
||||
@Test
|
||||
public void testDeviceType() {
|
||||
assertEquals(Device.DeviceType.Phone, Device.DeviceType.FromString(Device.DeviceType.Phone.toString()));
|
||||
assertEquals(Device.DeviceType.Tablet, Device.DeviceType.FromString(Device.DeviceType.Tablet.toString()));
|
||||
assertEquals(Device.DeviceType.Computer, Device.DeviceType.FromString(Device.DeviceType.Computer.toString()));
|
||||
assertEquals(Device.DeviceType.Tv, Device.DeviceType.FromString(Device.DeviceType.Tv.toString()));
|
||||
assertEquals(Device.DeviceType.Computer, Device.DeviceType.FromString(""));
|
||||
assertEquals(Device.DeviceType.Computer, Device.DeviceType.FromString(null));
|
||||
assertEquals(DeviceType.Phone, DeviceType.fromString(DeviceType.Phone.toString()));
|
||||
assertEquals(DeviceType.Tablet, DeviceType.fromString(DeviceType.Tablet.toString()));
|
||||
assertEquals(DeviceType.Computer, DeviceType.fromString(DeviceType.Computer.toString()));
|
||||
assertEquals(DeviceType.Tv, DeviceType.fromString(DeviceType.Tv.toString()));
|
||||
assertEquals(DeviceType.Computer, DeviceType.fromString("invalid"));
|
||||
}
|
||||
|
||||
// Basic paired device testing
|
||||
@@ -124,10 +123,10 @@ public class DeviceTest {
|
||||
Device device = new Device(context, "testDevice");
|
||||
|
||||
assertEquals(device.getDeviceId(), "testDevice");
|
||||
assertEquals(device.getDeviceType(), Device.DeviceType.Phone);
|
||||
assertEquals(device.getDeviceType(), DeviceType.Phone);
|
||||
assertEquals(device.getName(), "Test Device");
|
||||
assertTrue(device.isPaired());
|
||||
assertNotNull(device.certificate);
|
||||
assertNotNull(device.deviceInfo.certificate);
|
||||
}
|
||||
|
||||
public void testPairingDone() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, CertificateException {
|
||||
@@ -137,7 +136,7 @@ public class DeviceTest {
|
||||
fakeNetworkPacket.set("deviceId", deviceId);
|
||||
fakeNetworkPacket.set("deviceName", "Unpaired Test Device");
|
||||
fakeNetworkPacket.set("protocolVersion", DeviceHelper.ProtocolVersion);
|
||||
fakeNetworkPacket.set("deviceType", Device.DeviceType.Phone.toString());
|
||||
fakeNetworkPacket.set("deviceType", DeviceType.Phone.toString());
|
||||
String certificateString =
|
||||
"MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2\n" +
|
||||
"NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u\n" +
|
||||
@@ -157,18 +156,21 @@ public class DeviceTest {
|
||||
"7n+KOQ==";
|
||||
byte[] certificateBytes = Base64.decode(certificateString, 0);
|
||||
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
|
||||
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(fakeNetworkPacket, certificate);
|
||||
|
||||
LanLinkProvider linkProvider = Mockito.mock(LanLinkProvider.class);
|
||||
Mockito.when(linkProvider.getName()).thenReturn("LanLinkProvider");
|
||||
LanLink link = Mockito.mock(LanLink.class);
|
||||
Mockito.when(link.getLinkProvider()).thenReturn(linkProvider);
|
||||
Device device = new Device(context, deviceId, certificate, fakeNetworkPacket, link);
|
||||
Mockito.when(link.getDeviceId()).thenReturn(deviceId);
|
||||
Mockito.when(link.getDeviceInfo()).thenReturn(deviceInfo);
|
||||
Device device = new Device(context, link);
|
||||
|
||||
assertNotNull(device);
|
||||
assertEquals(device.getDeviceId(), deviceId);
|
||||
assertEquals(device.getName(), "Unpaired Test Device");
|
||||
assertEquals(device.getDeviceType(), Device.DeviceType.Phone);
|
||||
assertNotNull(device.certificate);
|
||||
assertEquals(device.getDeviceType(), DeviceType.Phone);
|
||||
assertNotNull(device.deviceInfo.certificate);
|
||||
|
||||
Method method = PairingHandler.class.getDeclaredMethod("pairingDone");
|
||||
method.setAccessible(true);
|
||||
|
@@ -10,10 +10,7 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
@@ -22,10 +19,13 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.internal.util.collections.Sets;
|
||||
import org.powermock.api.mockito.PowerMockito;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import java.security.cert.Certificate;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({DeviceHelper.class, Log.class})
|
||||
public class NetworkPacketTest {
|
||||
@@ -34,7 +34,7 @@ public class NetworkPacketTest {
|
||||
public void setUp() {
|
||||
PowerMockito.mockStatic(DeviceHelper.class);
|
||||
PowerMockito.when(DeviceHelper.getDeviceId(any())).thenReturn("123");
|
||||
PowerMockito.when(DeviceHelper.getDeviceType(any())).thenReturn(Device.DeviceType.Phone);
|
||||
PowerMockito.when(DeviceHelper.getDeviceType(any())).thenReturn(DeviceType.Phone);
|
||||
|
||||
PowerMockito.mockStatic(Log.class);
|
||||
}
|
||||
@@ -70,14 +70,23 @@ public class NetworkPacketTest {
|
||||
|
||||
@Test
|
||||
public void testIdentity() {
|
||||
Certificate cert = Mockito.mock(Certificate.class);
|
||||
|
||||
Context context = Mockito.mock(Context.class);
|
||||
MockSharedPreference settings = new MockSharedPreference();
|
||||
Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(settings);
|
||||
DeviceInfo deviceInfo = new DeviceInfo("myid", cert, "myname", DeviceType.Tv, 12, Sets.newSet("ASDFG"), Sets.newSet("QWERTY"));
|
||||
|
||||
NetworkPacket np = NetworkPacket.createIdentityPacket(context);
|
||||
NetworkPacket np = deviceInfo.toIdentityPacket();
|
||||
|
||||
assertEquals(np.getInt("protocolVersion"), 12);
|
||||
|
||||
DeviceInfo parsed = DeviceInfo.fromIdentityPacketAndCert(np, cert);
|
||||
|
||||
assertEquals(parsed.name, deviceInfo.name);
|
||||
assertEquals(parsed.id, deviceInfo.id);
|
||||
assertEquals(parsed.type, deviceInfo.type);
|
||||
assertEquals(parsed.protocolVersion, deviceInfo.protocolVersion);
|
||||
assertEquals(parsed.incomingCapabilities, deviceInfo.incomingCapabilities);
|
||||
assertEquals(parsed.outgoingCapabilities, deviceInfo.outgoingCapabilities);
|
||||
|
||||
assertEquals(np.getInt("protocolVersion"), DeviceHelper.ProtocolVersion);
|
||||
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user