mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-09-01 06:35:09 +00:00
Compare commits
40 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
268bc833be | ||
|
d62a7fbcdc | ||
|
8c9fc6586b | ||
|
0d658e6fb6 | ||
|
020382931c | ||
|
cc0b94bd3d | ||
|
5c0c190f5a | ||
|
41e296b16d | ||
|
8eb35028a1 | ||
|
f9486204a5 | ||
|
46c32365ba | ||
|
9dfa5bc51c | ||
|
fbf77fa103 | ||
|
06a486d99b | ||
|
2072128a6f | ||
|
a7dc9e4249 | ||
|
5a27a613ea | ||
|
ad48a25d79 | ||
|
2a273ff07f | ||
|
bd0b03eafb | ||
|
40b791a7c4 | ||
|
867bdfb6fb | ||
|
51312f9a25 | ||
|
e4743002be | ||
|
12de65f234 | ||
|
6d089093e9 | ||
|
ffd99858e6 | ||
|
6879e40341 | ||
|
cf28c9c7dc | ||
|
9d1cd05ce4 | ||
|
3e595cb262 | ||
|
636c70ff06 | ||
|
d68ccd69e0 | ||
|
a9e8050aeb | ||
|
81270f724d | ||
|
1ef3d75eb1 | ||
|
37c8a41778 | ||
|
ad9d375299 | ||
|
d6647e44b9 | ||
|
1d15cdba27 |
14
fastlane/metadata/android/az-AZ/full_description.txt
Normal file
14
fastlane/metadata/android/az-AZ/full_description.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
KDE connect cihazlarınız arasında inteqrasiya üçün funksiyalar dəstini təqdim edir:
|
||||
|
||||
- Mübadilə yaddaşının paylaşılması: cihazlarınız arasında kopyalayın və yerləşdirin.
|
||||
- İstənilən tətbiqdən komputeriniz ilə URL ünvanlarını və faylları paylaşın.
|
||||
- Kompyyuterinizdə gələn zənglər və SMS ismarıcları haqqında bildirişlər alın.
|
||||
- Virtual toxunma paneli: Telefonunuzun ektranını kompyuterin toxunma paneli kimi istifdə edin.
|
||||
- Bildirişlərin eyniləşdirilməsi: Android bildirişlərinizi kompyuterinizin iş masasından ozuyun.
|
||||
- Multimedianın məsafədən idarə edilməsi: Linux media oxuducusunu telefonunuzdan idarə edin.
|
||||
- WiFi bağlantısı: USB qoşulması və ya Bluetooth qoşulmasına ehtiyyac yoxdur.
|
||||
- Ucdan-uca TLC şifrələmə: məlumatlarınızın təhlükəsizliyi qorunur.
|
||||
|
||||
Nəzərə alın ki, bu tətbiqin işləməsi üçün kompyuterinizə KDE Connect-i quraşdırmalısınız və sonuncu funksiyaların işləməsi üçün İş masası və Android versiyalarını sonuncu versiyaya eyni şəkildə yeniləməlisiniz.
|
||||
|
||||
Bu tətbiq açıq qaynaq layihəsinin bir hissəsidir və ona töhvə verənlərin sayəsində mövcuddur. Mənbə kodunu əldə etmək üçün veb-səhifəyə daxil olun.
|
1
fastlane/metadata/android/az-AZ/short_description.txt
Normal file
1
fastlane/metadata/android/az-AZ/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect kompyuteriniz ilə smartfonunuzu inteqrasiya edir
|
1
fastlane/metadata/android/az-AZ/title.txt
Normal file
1
fastlane/metadata/android/az-AZ/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect
|
14
fastlane/metadata/android/en_GB/full_description.txt
Normal file
14
fastlane/metadata/android/en_GB/full_description.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
KDE Connect provides a set of features to integrate your workflow across devices:
|
||||
|
||||
- Shared clipboard: copy and paste between your devices.
|
||||
- Share files and URLs to your computer from any app.
|
||||
- Get notifications for incoming calls and SMS messages on your PC.
|
||||
- Virtual touchpad: Use your phone screen as your computer's touchpad.
|
||||
- Notifications sync: Read your Android notifications from the desktop.
|
||||
- Multimedia remote control: Use your phone as a remote for Linux media players.
|
||||
- WiFi connection: no USB wire or bluetooth needed.
|
||||
- End-to-end TLS encryption: your information is safe.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
1
fastlane/metadata/android/en_GB/short_description.txt
Normal file
1
fastlane/metadata/android/en_GB/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect integrates your smartphone and computer
|
1
fastlane/metadata/android/en_GB/title.txt
Normal file
1
fastlane/metadata/android/en_GB/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect
|
14
fastlane/metadata/android/sl/full_description.txt
Normal file
14
fastlane/metadata/android/sl/full_description.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
KDE Connect ponuja niz funkcij za integracijo delovnega procesa na različnih napravah:
|
||||
|
||||
- skupno odložišče: kopirajte in lepite med napravami;
|
||||
- datoteke in URL-je lahko z računalnikom delite iz poljubnega programa;
|
||||
- prejemanje obvestil o dohodnih klicih in sporočilih SMS na računalniku;
|
||||
- virtualna sledilna plošča: uporabite zaslon telefona kot sledilno tablico na računalniku;
|
||||
- sinhronizacija obvestil: preberite obvestila iz sistema Android na namizju;
|
||||
- večpredstavnostni daljinski upravljalnik: uporabite telefon kot daljinski upravljalnik za večpredstavnostne predvajalnike na Linuxu;
|
||||
- povezava WiFi: ne potrebujete žice USB ali bluetootha;
|
||||
- šifriranje TLS od enega konca do drugega: vaši podatki so varni.
|
||||
|
||||
Upoštevajte, da morate za delovanje tega programa na računalnik namestiti program KDE Connect in posodobiti namizno različico z različico za Android, da bodo delovale najnovejše funkcije.
|
||||
|
||||
Ta program je del odprto-kodnega projekta in obstaja po zaslugi vseh ljudi, ki so prispevali. Obiščite spletno mesto in si zagotovite izvorno kodo.
|
1
fastlane/metadata/android/sl/short_description.txt
Normal file
1
fastlane/metadata/android/sl/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect integrira vaš pametni telefon in računalnik
|
1
fastlane/metadata/android/sl/title.txt
Normal file
1
fastlane/metadata/android/sl/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect
|
14
fastlane/metadata/android/zh_CN/full_description.txt
Normal file
14
fastlane/metadata/android/zh_CN/full_description.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
KDE Connect 提供了一系列用于整合不同设备的功能特性:
|
||||
|
||||
- 剪贴板共享:跨设备复制粘贴内容。
|
||||
- 共享任意应用的文件和 URL 到电脑。
|
||||
- 在电脑上获取关于来电和短信的通知。
|
||||
- 虚拟触摸板:将手机屏幕当作电脑的触摸板使用。
|
||||
- 提醒同步:在电脑桌面端读取安卓端的通知。
|
||||
- 多媒体远程控制:用智能手机遥控 Linux 媒体播放器。
|
||||
- WiFi 连接:无需 USB 线或者蓝牙。
|
||||
- 端到端的 TLS 加密:确保您的信息安全。
|
||||
|
||||
请注意:您需要在您的电脑上安装 KDE Connect 才能使这款应用正常工作。请保持桌面端和安卓端的 KDE Connect 同步更新到一致的版本以便使用它们的最新功能。
|
||||
|
||||
此应用是一个自由开源软件项目的一部分。它的存续有赖于所有为它做出过贡献的人士。请访问项目主页以获取它的源代码。
|
1
fastlane/metadata/android/zh_CN/short_description.txt
Normal file
1
fastlane/metadata/android/zh_CN/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect 可以整合您的智能手机和电脑
|
1
fastlane/metadata/android/zh_CN/title.txt
Normal file
1
fastlane/metadata/android/zh_CN/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect
|
63
po/az/kdeconnect-android-store-full.po
Normal file
63
po/az/kdeconnect-android-store-full.po
Normal file
@@ -0,0 +1,63 @@
|
||||
# Kheyyam <xxmn77@gmail.com>, 2023.
|
||||
#. extracted from ./metadata/android/en-US/full_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-12 16:45+0400\n"
|
||||
"Last-Translator: Kheyyam <xxmn77@gmail.com>\n"
|
||||
"Language-Team: Azerbaijani <kde-i18n-doc@kde.org>\n"
|
||||
"Language: az\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Lokalize 23.04.2\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 cihazlarınız arasında inteqrasiya üçün funksiyalar dəstini "
|
||||
"təqdim edir:\n"
|
||||
"\n"
|
||||
"- Mübadilə yaddaşının paylaşılması: cihazlarınız arasında kopyalayın və "
|
||||
"yerləşdirin.\n"
|
||||
"- İstənilən tətbiqdən komputeriniz ilə URL ünvanlarını və faylları "
|
||||
"paylaşın.\n"
|
||||
"- Kompyyuterinizdə gələn zənglər və SMS ismarıcları haqqında bildirişlər "
|
||||
"alın.\n"
|
||||
"- Virtual toxunma paneli: Telefonunuzun ektranını kompyuterin toxunma paneli "
|
||||
"kimi istifdə edin.\n"
|
||||
"- Bildirişlərin eyniləşdirilməsi: Android bildirişlərinizi kompyuterinizin "
|
||||
"iş masasından ozuyun.\n"
|
||||
"- Multimedianın məsafədən idarə edilməsi: Linux media oxuducusunu "
|
||||
"telefonunuzdan idarə edin.\n"
|
||||
"- WiFi bağlantısı: USB qoşulması və ya Bluetooth qoşulmasına ehtiyyac "
|
||||
"yoxdur.\n"
|
||||
"- Ucdan-uca TLC şifrələmə: məlumatlarınızın təhlükəsizliyi qorunur.\n"
|
||||
"\n"
|
||||
"Nəzərə alın ki, bu tətbiqin işləməsi üçün kompyuterinizə KDE Connect-i "
|
||||
"quraşdırmalısınız və sonuncu funksiyaların işləməsi üçün İş masası və "
|
||||
"Android versiyalarını sonuncu versiyaya eyni şəkildə yeniləməlisiniz.\n"
|
||||
"\n"
|
||||
"Bu tətbiq açıq qaynaq layihəsinin bir hissəsidir və ona töhvə verənlərin "
|
||||
"sayəsində mövcuddur. Mənbə kodunu əldə etmək üçün veb-səhifəyə daxil olun."
|
19
po/az/kdeconnect-android-store-short.po
Normal file
19
po/az/kdeconnect-android-store-short.po
Normal file
@@ -0,0 +1,19 @@
|
||||
# Kheyyam <xxmn77@gmail.com>, 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-12 16:46+0400\n"
|
||||
"Last-Translator: Kheyyam <xxmn77@gmail.com>\n"
|
||||
"Language-Team: Azerbaijani <kde-i18n-doc@kde.org>\n"
|
||||
"Language: az\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Lokalize 23.04.2\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "KDE Connect integrates your smartphone and computer"
|
||||
msgstr "KDE Connect kompyuteriniz ilə smartfonunuzu inteqrasiya edir"
|
@@ -1,81 +0,0 @@
|
||||
# KDE Connect store listing texts
|
||||
# Copyright (C) 2014 Albert Vaca Cintora
|
||||
# This file is distributed under the same license as kdeconnect-android.
|
||||
#
|
||||
# Albert Vaca Cintora <albertvaka@gmail.com>, 2014.
|
||||
# Mincho Kondarev <mkondarev@yahoo.de>, 2023.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: https://bugs.kde.org\n"
|
||||
"POT-Creation-Date: 2019-06-30 11:38+0200\n"
|
||||
"PO-Revision-Date: 2023-06-08 22:08+0200\n"
|
||||
"Last-Translator: Mincho Kondarev <mkondarev@yahoo.de>\n"
|
||||
"Language: bg\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language-Team: Bulgarian <kde-i18n-doc@kde.org>\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Lokalize 23.04.1\n"
|
||||
|
||||
#: dummy:1
|
||||
msgid "Integrate Android with the KDE Plasma Desktop."
|
||||
msgstr "Интеграция на Android с работната среда Plasma на KDE."
|
||||
|
||||
#: dummy:2
|
||||
msgid ""
|
||||
"KDE Connect provides several features to integrate your phone and your"
|
||||
" computer:\n"
|
||||
"\n"
|
||||
"- Share files and URLs to KDE from any app, without wires.\n"
|
||||
"- Touchpad emulation: Use your phone screen as your computer's touchpad*.\n"
|
||||
"- Notifications sync (4.3+): Read your Android notifications from the"
|
||||
" desktop.\n"
|
||||
"- Shared clipboard: copy and paste between your phone and your computer.\n"
|
||||
"- Multimedia remote control: Use your phone as a remote for Linux media"
|
||||
" players.\n"
|
||||
"- WiFi connection: no usb wire or bluetooth needed.\n"
|
||||
"- RSA 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"
|
||||
"*NOTE for Ubuntu users: The Ubuntu folks are not updating their repos as fast"
|
||||
" as this app gets updated. Some features will not work if the KDE Connect"
|
||||
" version in you desktop doesn't match the one in your phone. To make sure you"
|
||||
" always have the latest version on your desktop, use this PPA repository: "
|
||||
" https://code.launchpad.net/~vikoadi/+archive/ubuntu/ppa/ \n"
|
||||
"\n"
|
||||
"This app is part of an open source project, visit the website to grab the"
|
||||
" sources.\n"
|
||||
msgstr ""
|
||||
"KDE Connect предоставя няколко функции за интегриране на телефона и компютъра"
|
||||
" ви:\n"
|
||||
"\n"
|
||||
"- Споделяне на файлове и URL адреси в KDE от всяко приложение, без кабели.\n"
|
||||
"- Емулация на тъчпад: Използвайте екрана на телефона си като тъчпад на"
|
||||
" компютъра*.\n"
|
||||
"- Синхронизиране на известия (4.3+): Четете известията си за Android от"
|
||||
" десктопа.\n"
|
||||
"- Споделен клипборд: копирайте и поставяйте между телефона и компютъра.\n"
|
||||
"- Мултимедийно дистанционно управление: Използвайте телефона си като"
|
||||
" дистанционно управление за Linux мултимедийни плейъри.\n"
|
||||
"- WiFi връзка: не е необходим USB кабел или Bluetooth.\n"
|
||||
"- RSA криптиране: информацията ви е в безопасност.\n"
|
||||
"\n"
|
||||
"Моля, обърнете внимание, че за да работи това приложение, трябва да"
|
||||
" инсталирате KDE Connect на компютъра си и да поддържате версията за настолни"
|
||||
" компютри актуализирана с версията за Android, за да работят най-новите"
|
||||
" функции.\n"
|
||||
"\n"
|
||||
"*Забележка за потребителите на Ubuntu: Ubuntu не актуализира своите хранилища"
|
||||
" толкова бързо, колкото се актуализира това приложение. Някои функции няма да"
|
||||
" работят, ако версията на KDE Connect в десктопа ви не съвпада с тази в"
|
||||
" телефона ви. За да сте сигурни, че винаги имате най-новата версия на"
|
||||
" десктопа си, използвайте това PPA хранилище:"
|
||||
" https://code.launchpad.net/~vikoadi/+archive/ubuntu/ppa/ \n"
|
||||
"\n"
|
||||
"Това приложение е част от проект с отворен код, посетете уебсайта, за да"
|
||||
" изтеглите изходния код.\n"
|
57
po/en_GB/kdeconnect-android-store-full.po
Normal file
57
po/en_GB/kdeconnect-android-store-full.po
Normal file
@@ -0,0 +1,57 @@
|
||||
# Steve Allewell <steve.allewell@gmail.com>, 2023.
|
||||
#. extracted from ./metadata/android/en-US/full_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-17 12:11+0100\n"
|
||||
"Last-Translator: Steve Allewell <steve.allewell@gmail.com>\n"
|
||||
"Language-Team: British English\n"
|
||||
"Language: en_GB\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Lokalize 23.03.70\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 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."
|
19
po/en_GB/kdeconnect-android-store-short.po
Normal file
19
po/en_GB/kdeconnect-android-store-short.po
Normal file
@@ -0,0 +1,19 @@
|
||||
# Steve Allewell <steve.allewell@gmail.com>, 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-17 12:11+0100\n"
|
||||
"Last-Translator: Steve Allewell <steve.allewell@gmail.com>\n"
|
||||
"Language-Team: British English\n"
|
||||
"Language: en_GB\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Lokalize 23.03.70\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
|
||||
msgid "KDE Connect integrates your smartphone and computer"
|
||||
msgstr "KDE Connect integrates your smartphone and computer"
|
@@ -13,7 +13,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Lokalize 20.04.2\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
|
||||
msgid "KDE Connect integrates your smartphone and computer"
|
||||
msgstr "KDE Connect integra tu teléfono inteligente y tu equipo"
|
||||
|
65
po/sl/kdeconnect-android-store-full.po
Normal file
65
po/sl/kdeconnect-android-store-full.po
Normal file
@@ -0,0 +1,65 @@
|
||||
# Translations template for KDEConnect.
|
||||
# Copyright (C) 2023 KDE
|
||||
# This file is distributed under the same license as the KDE project.
|
||||
#
|
||||
# Martin Srebotnjak <miles@filmsi.net>, 2023.
|
||||
#
|
||||
#. extracted from ./metadata/android/en-US/full_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-14 17:25+0200\n"
|
||||
"Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n"
|
||||
"Language-Team: Slovenian <kde-i18n-doc@kde.org>\n"
|
||||
"Language: sl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.2.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n"
|
||||
"%100==4 ? 3 : 0);\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 ponuja niz funkcij za integracijo delovnega procesa na različnih "
|
||||
"napravah:\n"
|
||||
"\n"
|
||||
"- skupno odložišče: kopirajte in lepite med napravami;\n"
|
||||
"- datoteke in URL-je lahko z računalnikom delite iz poljubnega programa;\n"
|
||||
"- prejemanje obvestil o dohodnih klicih in sporočilih SMS na računalniku;\n"
|
||||
"- virtualna sledilna plošča: uporabite zaslon telefona kot sledilno tablico "
|
||||
"na računalniku;\n"
|
||||
"- sinhronizacija obvestil: preberite obvestila iz sistema Android na "
|
||||
"namizju;\n"
|
||||
"- večpredstavnostni daljinski upravljalnik: uporabite telefon kot daljinski "
|
||||
"upravljalnik za večpredstavnostne predvajalnike na Linuxu;\n"
|
||||
"- povezava WiFi: ne potrebujete žice USB ali bluetootha;\n"
|
||||
"- šifriranje TLS od enega konca do drugega: vaši podatki so varni.\n"
|
||||
"\n"
|
||||
"Upoštevajte, da morate za delovanje tega programa na računalnik namestiti "
|
||||
"program KDE Connect in posodobiti namizno različico z različico za Android, "
|
||||
"da bodo delovale najnovejše funkcije.\n"
|
||||
"\n"
|
||||
"Ta program je del odprto-kodnega projekta in obstaja po zaslugi vseh ljudi, "
|
||||
"ki so prispevali. Obiščite spletno mesto in si zagotovite izvorno kodo."
|
25
po/sl/kdeconnect-android-store-short.po
Normal file
25
po/sl/kdeconnect-android-store-short.po
Normal file
@@ -0,0 +1,25 @@
|
||||
# Translations template for KDEConnect.
|
||||
# Copyright (C) 2023 KDE
|
||||
# This file is distributed under the same license as the KDE project.
|
||||
#
|
||||
# Martin Srebotnjak <miles@filmsi.net>, 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-14 17:26+0200\n"
|
||||
"Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n"
|
||||
"Language-Team: Slovenian <kde-i18n-doc@kde.org>\n"
|
||||
"Language: sl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.2.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n"
|
||||
"%100==4 ? 3 : 0);\n"
|
||||
|
||||
msgid "KDE Connect integrates your smartphone and computer"
|
||||
msgstr "KDE Connect integrira vaš pametni telefon in računalnik"
|
59
po/zh_CN/kdeconnect-android-store-full.po
Normal file
59
po/zh_CN/kdeconnect-android-store-full.po
Normal file
@@ -0,0 +1,59 @@
|
||||
#. extracted from ./metadata/android/en-US/full_description.txt
|
||||
msgid ""
|
||||
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"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Translate Toolkit 2.5.0\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: kdeorg\n"
|
||||
"X-Crowdin-Project-ID: 269464\n"
|
||||
"X-Crowdin-Language: zh-CN\n"
|
||||
"X-Crowdin-File: /kf5-trunk/messages/kdeconnect-android/kdeconnect-android-"
|
||||
"store-full.pot\n"
|
||||
"X-Crowdin-File-ID: 43897\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 提供了一系列用于整合不同设备的功能特性:\n"
|
||||
"\n"
|
||||
"- 剪贴板共享:跨设备复制粘贴内容。\n"
|
||||
"- 共享任意应用的文件和 URL 到电脑。\n"
|
||||
"- 在电脑上获取关于来电和短信的通知。\n"
|
||||
"- 虚拟触摸板:将手机屏幕当作电脑的触摸板使用。\n"
|
||||
"- 提醒同步:在电脑桌面端读取安卓端的通知。\n"
|
||||
"- 多媒体远程控制:用智能手机遥控 Linux 媒体播放器。\n"
|
||||
"- WiFi 连接:无需 USB 线或者蓝牙。\n"
|
||||
"- 端到端的 TLS 加密:确保您的信息安全。\n"
|
||||
"\n"
|
||||
"请注意:您需要在您的电脑上安装 KDE Connect 才能使这款应用正常工作。请保持桌面"
|
||||
"端和安卓端的 KDE Connect 同步更新到一致的版本以便使用它们的最新功能。\n"
|
||||
"\n"
|
||||
"此应用是一个自由开源软件项目的一部分。它的存续有赖于所有为它做出过贡献的人"
|
||||
"士。请访问项目主页以获取它的源代码。"
|
24
po/zh_CN/kdeconnect-android-store-short.po
Normal file
24
po/zh_CN/kdeconnect-android-store-short.po
Normal file
@@ -0,0 +1,24 @@
|
||||
#. extracted from ./metadata/android/en-US/short_description.txt
|
||||
msgid ""
|
||||
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"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Translate Toolkit 2.5.0\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Crowdin-Project: kdeorg\n"
|
||||
"X-Crowdin-Project-ID: 269464\n"
|
||||
"X-Crowdin-Language: zh-CN\n"
|
||||
"X-Crowdin-File: /kf5-trunk/messages/kdeconnect-android/kdeconnect-android-"
|
||||
"store-short.pot\n"
|
||||
"X-Crowdin-File-ID: 43899\n"
|
||||
|
||||
msgid "KDE Connect integrates your smartphone and computer"
|
||||
msgstr "KDE Connect 可以整合您的智能手机和电脑"
|
@@ -108,6 +108,7 @@
|
||||
<string name="device_menu_plugins">Plaqin ayarları</string>
|
||||
<string name="device_menu_unpair">Ayırmaq</string>
|
||||
<string name="pair_new_device">Yeni cihaz qoşmaq</string>
|
||||
<string name="cancel_pairing">Qoşulmanı ləğv edin</string>
|
||||
<string name="unknown_device">Naməlum Cihaz</string>
|
||||
<string name="error_not_reachable">Cihaz əlçatmazdır</string>
|
||||
<string name="error_already_paired">Cihaz artıq qoşulub</string>
|
||||
@@ -393,6 +394,7 @@
|
||||
<string name="holger_kaelberer_task">Uzaq klaviatura əlavəsi və xəta sazlamaları</string>
|
||||
<string name="saikrishna_arcot_task">Uzaq daxiletmə əlavəsində klaviaturanın istifadəsini dəstəklənməsi, xəta sazlamaları və əsas yaxşılaşdırmalar</string>
|
||||
<string name="everyone_else">İllər boyu KDE Connect\'ə töhfə verən hər kəs</string>
|
||||
<string name="send_clipboard">Mübadilə buferinə göndərin</string>
|
||||
<string name="send_clipboard">Mübadilə yaddaşını göndərin</string>
|
||||
<string name="tap_to_execute">İcra etmək üçün vurun</string>
|
||||
<string name="plugin_stats">Plaqin statistikası</string>
|
||||
</resources>
|
||||
|
@@ -108,6 +108,7 @@
|
||||
<string name="device_menu_plugins">Plugin settings</string>
|
||||
<string name="device_menu_unpair">Unpair</string>
|
||||
<string name="pair_new_device">Pair new device</string>
|
||||
<string name="cancel_pairing">Cancel pairing</string>
|
||||
<string name="unknown_device">Unknown device</string>
|
||||
<string name="error_not_reachable">Device not reachable</string>
|
||||
<string name="error_already_paired">Device already paired</string>
|
||||
@@ -395,4 +396,5 @@
|
||||
<string name="everyone_else">Everyone else who has contributed to KDE Connect over the years</string>
|
||||
<string name="send_clipboard">Send clipboard</string>
|
||||
<string name="tap_to_execute">Tap to execute</string>
|
||||
<string name="plugin_stats">Plugin stats</string>
|
||||
</resources>
|
||||
|
@@ -33,7 +33,7 @@
|
||||
<string name="pref_plugin_notifications">Sincronización de notificacións</string>
|
||||
<string name="pref_plugin_notifications_desc">Acceda ás súas notificacións desde outros dispositivos</string>
|
||||
<string name="pref_plugin_receive_notifications">Recibir notificacións</string>
|
||||
<string name="pref_plugin_receive_notifications_desc">Recibir notificacións do outro dispositivo e mostralas en Android</string>
|
||||
<string name="pref_plugin_receive_notifications_desc">Recibir notificacións do outro dispositivo e amosalas en Android</string>
|
||||
<string name="pref_plugin_sharereceiver">Compartir e recibir</string>
|
||||
<string name="pref_plugin_sharereceiver_desc">Comparta ficheiros e enderezos URL entre dispositivos</string>
|
||||
<string name="device_list_empty">Non hai dispositivos.</string>
|
||||
@@ -42,7 +42,7 @@
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="open_settings">Abrir a configuración</string>
|
||||
<string name="no_permissions">Debe conceder permisos para acceder ás notificacións</string>
|
||||
<string name="no_permission_mprisreceiver">Para poder controlar os seus reprodutores de son e vídeo ten que garantir acceso ás notificacións</string>
|
||||
<string name="no_permission_mprisreceiver">Para poder controlar os seus reprodutores multimedia ten que garantir acceso ás notificacións</string>
|
||||
<string name="no_permissions_remotekeyboard">Para recibir presións de tecla ten que activar o teclado remoto de KDE Connect</string>
|
||||
<string name="send_ping">Enviar un ping</string>
|
||||
<string name="open_mpris_controls">Control multimedia</string>
|
||||
@@ -57,7 +57,7 @@
|
||||
<string name="mousepad_double_tap_settings_title">Definir a acción de tocar con dous dedos</string>
|
||||
<string name="mousepad_triple_tap_settings_title">Definir a acción de tocar con tres dedos</string>
|
||||
<string name="mousepad_sensitivity_settings_title">Definir a sensibilidade do punteiro táctil</string>
|
||||
<string name="mousepad_mouse_buttons_title">Mostrar os botóns do rato</string>
|
||||
<string name="mousepad_mouse_buttons_title">Amosar os botóns do rato</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Definir a aceleración do punteiro</string>
|
||||
<string name="mousepad_scroll_direction_title">Inverter a dirección de desprazamento</string>
|
||||
<string name="gyro_mouse_enabled_title">Activar o rato de xiroscopio</string>
|
||||
@@ -159,7 +159,7 @@
|
||||
<string name="tap_to_answer">Toque para contestar</string>
|
||||
<string name="right_click">Enviar un clic secundario</string>
|
||||
<string name="middle_click">Enviar un clic central</string>
|
||||
<string name="show_keyboard">Mostrar o teclado</string>
|
||||
<string name="show_keyboard">Amosar o teclado</string>
|
||||
<string name="device_not_paired">O dispositivo non está emparellado</string>
|
||||
<string name="request_pairing">Solicitar emparellarse</string>
|
||||
<string name="pairing_accept">Aceptar</string>
|
||||
@@ -183,7 +183,7 @@
|
||||
<item>1 minuto</item>
|
||||
<item>2 minutos</item>
|
||||
</string-array>
|
||||
<string name="mpris_notification_settings_title">Mostrar a notificación de control de reprodución.</string>
|
||||
<string name="mpris_notification_settings_title">Amosar a notificación de control de reprodución.</string>
|
||||
<string name="mpris_notification_settings_summary">Permitir controlar os reprodutores sen abrir KDE Connect</string>
|
||||
<string name="share_to">Compartir con…</string>
|
||||
<string name="protocol_version_newer">Este dispositivo usa unha versión máis nova do protocolo.</string>
|
||||
@@ -215,9 +215,9 @@
|
||||
<string name="sftp_storage_preference_storage_location">Lugar de almacenamento</string>
|
||||
<string name="sftp_storage_preference_storage_location_already_configured">Este lugar xa está configurado</string>
|
||||
<string name="sftp_storage_preference_click_to_select">premer para seleccionar</string>
|
||||
<string name="sftp_storage_preference_display_name">Nome para mostrar</string>
|
||||
<string name="sftp_storage_preference_display_name_already_used">Este nome para mostrar xa está a usarse</string>
|
||||
<string name="sftp_storage_preference_display_name_cannot_be_empty">O nome para mostrar non pode estar baleiro</string>
|
||||
<string name="sftp_storage_preference_display_name">Nome para amosar</string>
|
||||
<string name="sftp_storage_preference_display_name_already_used">Este nome para amosar xa está a usarse</string>
|
||||
<string name="sftp_storage_preference_display_name_cannot_be_empty">O nome para amosar non pode estar baleiro</string>
|
||||
<string name="sftp_action_mode_menu_delete">Eliminar</string>
|
||||
<string name="sftp_no_storage_locations_configured">Non se configuraron localizacións de almacenamento</string>
|
||||
<string name="sftp_saf_permission_explanation">Para acceder a ficheiro remotamente ten que configurar lugares de almacenamento</string>
|
||||
@@ -242,7 +242,7 @@
|
||||
<string name="findmyphone_title">Atopar o móbil</string>
|
||||
<string name="findmyphone_title_tablet">Atopar a tableta</string>
|
||||
<string name="findmyphone_title_tv">Atopar o meu televisor</string>
|
||||
<string name="findmyphone_description">Reproduce un son de chamada no dispositivo para que poida atopalo.</string>
|
||||
<string name="findmyphone_description">Reproduce un ton de chamada no dispositivo para que poida atopalo.</string>
|
||||
<string name="findmyphone_found">Atopeino</string>
|
||||
<string name="open">Abrir</string>
|
||||
<string name="close">Pechar</string>
|
||||
@@ -256,9 +256,9 @@
|
||||
<string name="telephony_permission_explanation">Para ver as chamadas de teléfono no escritorio ten que dar permiso aos rexistros de chamadas telefónicas e ao estado do teléfono</string>
|
||||
<string name="telephony_optional_permission_explanation">Para ver o nome dun contacto en vez dun número de teléfono ten que dar acceso aos contactos do teléfono.</string>
|
||||
<string name="contacts_permission_explanation">Para compartir o caderno de contactos co escritorio ten que dar permiso de contactos</string>
|
||||
<string name="select_ringtone">Seleccione un son de chamada</string>
|
||||
<string name="select_ringtone">Seleccione un ton de chamada</string>
|
||||
<string name="telephony_pref_blocked_title">Números bloqueados</string>
|
||||
<string name="telephony_pref_blocked_dialog_desc">Non mostrar chamadas nin SMS destes números. Indique un número por liña.</string>
|
||||
<string name="telephony_pref_blocked_dialog_desc">Non amosar chamadas nin SMS destes números. Indique un número por liña.</string>
|
||||
<string name="mpris_coverart_description">Portada da obra actual.</string>
|
||||
<string name="device_icon_description">Icona do dispositivo.</string>
|
||||
<string name="settings_icon_description">Icona da configuración.</string>
|
||||
@@ -295,8 +295,8 @@
|
||||
<string name="settings_rename">Nome do dispositivo</string>
|
||||
<string name="settings_dark_mode">Tema escuro</string>
|
||||
<string name="settings_more_settings_title">Máis opcións</string>
|
||||
<string name="settings_more_settings_text">As opcións específicas dun dispositivo están en «Configuración dos complementos» no dispositivo.</string>
|
||||
<string name="setting_persistent_notification">Mostrar unha notificación persistente</string>
|
||||
<string name="settings_more_settings_text">As opcións específicas dun dispositivo atópanse en «Configuración dos complementos» no dispositivo.</string>
|
||||
<string name="setting_persistent_notification">Amosar unha notificación persistente</string>
|
||||
<string name="setting_persistent_notification_oreo">Notificación persistente</string>
|
||||
<string name="setting_persistent_notification_description">Toque para activar ou desactivar na configuración de notificacións</string>
|
||||
<string name="extra_options">Opcións adicionais</string>
|
||||
@@ -381,7 +381,7 @@
|
||||
<string name="open_compose_send">Escribir texto</string>
|
||||
<string name="about_kde_about">"<h1>Sobre</h1> <p>KDE é unha comunidade internacional de persoas dedicadas á enxeñaría de software, á arte, á documentación, á tradución e á creación, todas elas comprometidas co desenvolvemento de <a href=https://www.gnu.org/philosophy/free-sw.html>software libre</a>. KDE produce o ambiente de escritorio Plasma, centos de aplicacións, e as moitas bibliotecas de software sobre as que estas están construídas.</p> <p>KDE é un esforzo cooperativo: non hai unha única entidade que controle a súa dirección ou os seus produtos. No seu lugar, xuntámonos para traballar no obxectivo común de construír o mellor software libre do mundo. Todas as persoas son benvidas a <a href=https://community.kde.org/Get_Involved>unirse e colaborar</a> en KDE, incluída vostede.</p> Visite <a href=https://www.kde.org/>https://www.kde.org/</a> para máis información sobre a comunidade KDE e o software que produce."</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Informe de fallos ou pida melloras</h1> <p>O software sempre pode mellorarse, e o equipo de KDE está preparado para facelo. Porén, vostede, a persoa usuaria, ten que avisarnos cando algo non funciona como espera ou podería mellorarse.</p> <p>KDE ten un sistema de seguimento de fallos. Visite <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> ou use o botón de «Informar dun fallo» da pantalla de información para informar dun fallo.</p> Se ten unha suxestión de mellora tamén pode usar o sistema de seguimento de fallos para rexistrala. Asegúrese nese caso de usar a severidade «Lista de desexos».</string>
|
||||
<string name="about_kde_join_kde"><h1>Únase a KDE</h1> <p>Non necesita coñecementos de enxeñaría de software para formar parte do equipo de KDE. Pode unirse aos equipos nacionais que traducen as interfaces dos programas. Pode crear imaxes, temas, sons, e mellorar a documentación. Vostede decide!</p> <p>Visite <a href=https://community.kde.org/Get_Involved>https://community.kde.org/Get_Involved</a> para informarse sobre os proxectos nos que pode participar.</p> Se necesita máis información ou documentación, atopará o que necesita en <a href=https://techbase.kde.org/>https://techbase.kde.org/</a>.</string>
|
||||
<string name="about_kde_join_kde"><h1>Únase a KDE</h1> <p>Non necesita saber desenvolver software para formar parte do equipo de KDE. Pode unirse aos equipos nacionais que traducen as interfaces dos programas. Pode crear imaxes, temas, sons, e mellorar a documentación. Vostede decide!</p> <p>Visite <a href=https://community.kde.org/Get_Involved>https://community.kde.org/Get_Involved</a> para informarse sobre os proxectos nos que pode participar.</p> Se necesita máis información ou documentación, ten o que necesita en <a href=https://techbase.kde.org/>https://techbase.kde.org/</a>.</string>
|
||||
<string name="about_kde_support_kde"><h1>Apoie KDE</h1> <p>O software de KDE está e estará sempre dispoñíbel de balde, porén crealo ten custos.</p> <p>Para apoiar o seu desenvolvemento, a comunidade KDE formou o KDE e.V., unha organización sen ánimo de lucro fundada legalmente na Alemaña. KDE e.V. representa á comunidade KDE en asuntos legais e financeiros. Consulte <a href=https://ev.kde.org/>https://ev.kde.org/</a> para máis información sobre KDE e.V.</p> <p>KDE benefíciase de moitos tipos de contribucións, incluídas as monetarias. Usamos os fondos para cubrir gastos derivados de colaborar. A maiores, os fondos úsanse para asistencia legal e para organizar conferencias e encontros.</p> <p>Animámoslle a apoiar os nosos esforzos cunha doazón monetaria, mediante un dos sistemas detallados en <a href=https://www.kde.org/community/donations/>https://www.kde.org/community/donations/</a>.</p> Moitas grazas de antemán polo seu apoio.</string>
|
||||
<string name="maintainer_and_developer">Mantemento e desenvolvemento</string>
|
||||
<string name="developer">Desenvolvemento</string>
|
||||
|
@@ -194,4 +194,5 @@
|
||||
<string name="everyone_else">Alcun altere qui ha contribuite a KDE Connect durante le annos</string>
|
||||
<string name="send_clipboard">Invia Area de transferentia</string>
|
||||
<string name="tap_to_execute">Toccaper executar</string>
|
||||
<string name="plugin_stats">"Statisticas de plugin "</string>
|
||||
</resources>
|
||||
|
@@ -108,6 +108,7 @@
|
||||
<string name="device_menu_plugins">插件设置</string>
|
||||
<string name="device_menu_unpair">取消配对</string>
|
||||
<string name="pair_new_device">配对新设备</string>
|
||||
<string name="cancel_pairing">取消配对</string>
|
||||
<string name="unknown_device">未知设备</string>
|
||||
<string name="error_not_reachable">设备不可及</string>
|
||||
<string name="error_already_paired">设备已配对</string>
|
||||
@@ -387,4 +388,5 @@
|
||||
<string name="everyone_else">以及多年来为 KDE Connect 作出过贡献的其他所有人</string>
|
||||
<string name="send_clipboard">发送剪贴板</string>
|
||||
<string name="tap_to_execute">轻触执行</string>
|
||||
<string name="plugin_stats">插件状态</string>
|
||||
</resources>
|
||||
|
@@ -547,4 +547,6 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
|
||||
<string name="plugin_stats">Plugin stats</string>
|
||||
|
||||
<string name="enable_udp_broadcast">Enable backwards-compatible device discovery</string>
|
||||
|
||||
</resources>
|
||||
|
@@ -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,14 +55,14 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
linkProvider.connectionLost(this);
|
||||
linkProvider.onConnectionLost(this);
|
||||
}
|
||||
|
||||
//TO OVERRIDE, should be sync. If sendPayloadFromSameThread is false, it should only block to send the packet but start a separate thread to send the payload.
|
||||
|
@@ -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);
|
||||
}
|
||||
@@ -33,24 +27,26 @@ public abstract class BaseLinkProvider {
|
||||
return connectionReceivers.remove(cr);
|
||||
}
|
||||
|
||||
//These two should be called when the provider links to a new computer
|
||||
protected void connectionAccepted(@NonNull final String deviceId,
|
||||
@NonNull final Certificate certificate,
|
||||
@NonNull final NetworkPacket identityPacket,
|
||||
@NonNull final BaseLink link) {
|
||||
//Log.i("KDE/LinkProvider", "connectionAccepted");
|
||||
/**
|
||||
* To be called from the child classes when a link to a new device is established
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
protected void connectionLost(BaseLink link) {
|
||||
|
||||
/**
|
||||
* To be called from the child classes when a link to an existing device is disconnected
|
||||
*/
|
||||
public void onConnectionLost(BaseLink link) {
|
||||
//Log.i("KDE/LinkProvider", "connectionLost");
|
||||
for(ConnectionReceiver cr : connectionReceivers) {
|
||||
cr.onConnectionLost(link);
|
||||
}
|
||||
}
|
||||
|
||||
//To override
|
||||
public abstract void onStart();
|
||||
public abstract void onStop();
|
||||
public abstract void onNetworkChange();
|
||||
|
@@ -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;
|
||||
@@ -44,7 +46,7 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
|
||||
private static final int REQUEST_ENABLE_BT = 48;
|
||||
|
||||
private final Context context;
|
||||
private final Map<String, BluetoothLink> visibleComputers = new HashMap<>();
|
||||
private final Map<String, BluetoothLink> visibleDevices = new HashMap<>();
|
||||
private final Map<BluetoothDevice, BluetoothSocket> sockets = new HashMap<>();
|
||||
|
||||
private final BluetoothAdapter bluetoothAdapter;
|
||||
@@ -54,19 +56,16 @@ 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 = visibleComputers.get(deviceId);
|
||||
BluetoothLink oldLink = visibleDevices.get(deviceId);
|
||||
if (oldLink == link) {
|
||||
Log.e("BluetoothLinkProvider", "oldLink == link. This should not happen!");
|
||||
return;
|
||||
}
|
||||
visibleComputers.put(deviceId, link);
|
||||
connectionAccepted(deviceId, certificate, identityPacket, link);
|
||||
visibleDevices.put(deviceId, link);
|
||||
onConnectionReceived(link);
|
||||
link.startListening();
|
||||
link.packetReceived(identityPacket);
|
||||
if (oldLink != null) {
|
||||
Log.i("BluetoothLinkProvider", "Removing old connection to same device");
|
||||
oldLink.disconnect();
|
||||
@@ -127,10 +126,10 @@ 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);
|
||||
visibleComputers.remove(deviceId);
|
||||
connectionLost(link);
|
||||
visibleDevices.remove(link.getDeviceId());
|
||||
onConnectionLost(link);
|
||||
}
|
||||
|
||||
private class ServerRunnable implements Runnable {
|
||||
@@ -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,23 +367,31 @@ 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();
|
||||
return;
|
||||
}
|
||||
|
||||
if (visibleComputers.containsKey(identityPacket.getString("deviceId"))) {
|
||||
if (visibleDevices.containsKey(identityPacket.getString("deviceId"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@@ -14,7 +14,9 @@ import androidx.annotation.WorkerThread;
|
||||
|
||||
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;
|
||||
@@ -37,17 +39,13 @@ import kotlin.text.Charsets;
|
||||
|
||||
public class LanLink extends BaseLink {
|
||||
|
||||
public interface LinkDisconnectedCallback {
|
||||
void linkDisconnected(LanLink brokenLink);
|
||||
}
|
||||
|
||||
public enum ConnectionStarted {
|
||||
Locally, Remotely
|
||||
}
|
||||
|
||||
private volatile SSLSocket socket = null;
|
||||
private final DeviceInfo deviceInfo;
|
||||
|
||||
private final LinkDisconnectedCallback callback;
|
||||
private volatile SSLSocket socket = null;
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
@@ -96,7 +94,7 @@ public class LanLink extends BaseLink {
|
||||
boolean thereIsaANewSocket = (newSocket != socket);
|
||||
if (!thereIsaANewSocket) {
|
||||
Log.i("LanLink", "Socket closed and there's no new socket, disconnecting device");
|
||||
callback.linkDisconnected(LanLink.this);
|
||||
getLinkProvider().onConnectionLost(LanLink.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -104,19 +102,22 @@ public class LanLink extends BaseLink {
|
||||
return oldSocket;
|
||||
}
|
||||
|
||||
public LanLink(Context context, String deviceId, LanLinkProvider linkProvider, SSLSocket socket) throws IOException {
|
||||
super(context, deviceId, linkProvider);
|
||||
callback = 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);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "LanLink";
|
||||
}
|
||||
|
||||
//Blocking, do not call from main thread
|
||||
@Override
|
||||
public DeviceInfo getDeviceInfo() {
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Override
|
||||
public boolean sendPacket(@NonNull NetworkPacket np, @NonNull final Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {
|
||||
@@ -172,9 +173,7 @@ public class LanLink extends BaseLink {
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
if (callback != null) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
callback.onFailure(e);
|
||||
return false;
|
||||
} finally {
|
||||
//Make sure we close the payload stream, if any
|
||||
|
@@ -11,9 +11,13 @@ import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
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;
|
||||
@@ -21,6 +25,7 @@ import org.kde.kdeconnect.Helpers.TrustedNetworkHelper;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.UserInterface.CustomDevicesActivity;
|
||||
import org.kde.kdeconnect.UserInterface.SettingsFragment;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@@ -33,11 +38,11 @@ import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.List;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
@@ -45,43 +50,44 @@ import javax.net.ssl.SSLSocket;
|
||||
import kotlin.text.Charsets;
|
||||
|
||||
/**
|
||||
* This BaseLinkProvider creates {@link LanLink}s to other devices on the same
|
||||
* 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)
|
||||
*/
|
||||
public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDisconnectedCallback {
|
||||
public class LanLinkProvider extends BaseLinkProvider {
|
||||
|
||||
private final static int UDP_PORT = 1716;
|
||||
private final static int MIN_PORT = 1716;
|
||||
private final static int MAX_PORT = 1764;
|
||||
final static int PAYLOAD_TRANSFER_MIN_PORT = 1739;
|
||||
|
||||
final static int MAX_UDP_PACKET_SIZE = 1024 * 512;
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final HashMap<String, LanLink> visibleComputers = new HashMap<>(); //Links by device id
|
||||
final HashMap<String, LanLink> visibleDevices = new HashMap<>(); //Links by device id
|
||||
|
||||
private ServerSocket tcpServer;
|
||||
private DatagramSocket udpServer;
|
||||
ServerSocket tcpServer;
|
||||
DatagramSocket udpServer;
|
||||
|
||||
MdnsDiscovery mdnsDiscovery;
|
||||
|
||||
private long lastBroadcast = 0;
|
||||
private final static long delayBetweenBroadcasts = 200;
|
||||
|
||||
private boolean listening = false;
|
||||
|
||||
// To prevent infinte loop between Android < IceCream because both device can only broadcast identity packet but cannot connect via TCP
|
||||
private final ArrayList<InetAddress> reverseConnectionBlackList = new ArrayList<>();
|
||||
|
||||
@Override // SocketClosedCallback
|
||||
public void linkDisconnected(LanLink brokenLink) {
|
||||
String deviceId = brokenLink.getDeviceId();
|
||||
visibleComputers.remove(deviceId);
|
||||
connectionLost(brokenLink);
|
||||
public void onConnectionLost(BaseLink link) {
|
||||
String deviceId = link.getDeviceId();
|
||||
visibleDevices.remove(deviceId);
|
||||
super.onConnectionLost(link);
|
||||
}
|
||||
|
||||
//They received my UDP broadcast and are connecting to me. The first thing they sned should be their identity.
|
||||
private void tcpPacketReceived(Socket socket) {
|
||||
//They received my UDP broadcast and are connecting to me. The first thing they send should be their identity packet.
|
||||
@WorkerThread
|
||||
private void tcpPacketReceived(Socket socket) throws IOException {
|
||||
|
||||
NetworkPacket networkPacket;
|
||||
try {
|
||||
@@ -104,57 +110,41 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
}
|
||||
|
||||
//I've received their broadcast and should connect to their TCP socket and send my identity.
|
||||
private void udpPacketReceived(DatagramPacket packet) {
|
||||
@WorkerThread
|
||||
private void udpPacketReceived(DatagramPacket packet) throws JSONException, IOException {
|
||||
|
||||
final InetAddress address = packet.getAddress();
|
||||
|
||||
try {
|
||||
|
||||
String message = new String(packet.getData(), Charsets.UTF_8);
|
||||
final NetworkPacket identityPacket = NetworkPacket.unserialize(message);
|
||||
final String deviceId = identityPacket.getString("deviceId");
|
||||
if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
|
||||
Log.e("KDE/LanLinkProvider", "Expecting an UDP identity packet");
|
||||
String message = new String(packet.getData(), Charsets.UTF_8);
|
||||
final NetworkPacket identityPacket = NetworkPacket.unserialize(message);
|
||||
final String deviceId = identityPacket.getString("deviceId");
|
||||
if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
|
||||
Log.e("KDE/LanLinkProvider", "Expecting an UDP identity packet");
|
||||
return;
|
||||
} else {
|
||||
String myId = DeviceHelper.getDeviceId(context);
|
||||
if (deviceId.equals(myId)) {
|
||||
//Ignore my own broadcast
|
||||
return;
|
||||
} else {
|
||||
String myId = DeviceHelper.getDeviceId(context);
|
||||
if (deviceId.equals(myId)) {
|
||||
//Ignore my own broadcast
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("KDE/LanLinkProvider", "Broadcast identity packet received from " + identityPacket.getString("deviceName"));
|
||||
|
||||
int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT);
|
||||
|
||||
SocketFactory socketFactory = SocketFactory.getDefault();
|
||||
Socket socket = socketFactory.createSocket(address, tcpPort);
|
||||
configureSocket(socket);
|
||||
|
||||
OutputStream out = socket.getOutputStream();
|
||||
NetworkPacket myIdentity = NetworkPacket.createIdentityPacket(context);
|
||||
out.write(myIdentity.serialize().getBytes());
|
||||
out.flush();
|
||||
|
||||
identityPacketReceived(identityPacket, socket, LanLink.ConnectionStarted.Remotely);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e("KDE/LanLinkProvider", "Cannot connect to " + address, e);
|
||||
if (!reverseConnectionBlackList.contains(address)) {
|
||||
Log.w("KDE/LanLinkProvider", "Blacklisting " + address);
|
||||
reverseConnectionBlackList.add(address);
|
||||
new Timer().schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
reverseConnectionBlackList.remove(address);
|
||||
}
|
||||
}, 5 * 1000);
|
||||
|
||||
// Try to cause a reverse connection
|
||||
onNetworkChange();
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("KDE/LanLinkProvider", "Broadcast identity packet received from " + identityPacket.getString("deviceName"));
|
||||
|
||||
int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT);
|
||||
|
||||
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();
|
||||
out.write(myIdentity.serialize().getBytes());
|
||||
out.flush();
|
||||
|
||||
identityPacketReceived(identityPacket, socket, LanLink.ConnectionStarted.Remotely);
|
||||
}
|
||||
|
||||
private void configureSocket(Socket socket) {
|
||||
@@ -169,6 +159,8 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
* Called when a new 'identity' packet is received. Those are passed here by
|
||||
* {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}.
|
||||
* <p>
|
||||
* Should be called on a new thread since it blocks until the handshake is completed.
|
||||
* </p><p>
|
||||
* If the remote device should be connected, this calls {@link #addLink}.
|
||||
* Otherwise, if there was an Exception, we unpair from that device.
|
||||
* </p>
|
||||
@@ -177,7 +169,8 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
* @param socket a new Socket, which should be used to receive packets from the remote device
|
||||
* @param connectionStarted which side started this connection
|
||||
*/
|
||||
private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted) {
|
||||
@WorkerThread
|
||||
private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted) throws IOException {
|
||||
|
||||
String myId = DeviceHelper.getDeviceId(context);
|
||||
final String deviceId = identityPacket.getString("deviceId");
|
||||
@@ -189,93 +182,73 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
// If I'm the TCP server I will be the SSL client and viceversa.
|
||||
final boolean clientMode = (connectionStarted == LanLink.ConnectionStarted.Locally);
|
||||
|
||||
// Do the SSL handshake
|
||||
try {
|
||||
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
|
||||
boolean isDeviceTrusted = preferences.getBoolean(deviceId, false);
|
||||
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
|
||||
boolean isDeviceTrusted = preferences.getBoolean(deviceId, false);
|
||||
|
||||
if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) {
|
||||
//Device paired with and old version, we can't use it as we lack the certificate
|
||||
if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) {
|
||||
//Device paired with and old version, we can't use it as we lack the certificate
|
||||
Device device = KdeConnect.getInstance().getDevice(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
device.unpair();
|
||||
//Retry as unpaired
|
||||
identityPacketReceived(identityPacket, socket, connectionStarted);
|
||||
}
|
||||
|
||||
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];
|
||||
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;
|
||||
}
|
||||
device.unpair();
|
||||
//Retry as unpaired
|
||||
identityPacketReceived(identityPacket, socket, connectionStarted);
|
||||
}
|
||||
});
|
||||
|
||||
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("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);
|
||||
Device device = KdeConnect.getInstance().getDevice(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
device.unpair();
|
||||
}
|
||||
});
|
||||
//Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection
|
||||
ThreadHelper.execute(() -> {
|
||||
try {
|
||||
synchronized (this) {
|
||||
sslsocket.startHandshake();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("KDE/LanLinkProvider", "Handshake failed with " + identityPacket.getString("deviceName"), e);
|
||||
|
||||
//String[] ciphers = sslsocket.getSupportedCipherSuites();
|
||||
//for (String cipher : ciphers) {
|
||||
// Log.i("SupportedCiphers","cipher: " + cipher);
|
||||
//}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e("LanLink", "Exception", e);
|
||||
}
|
||||
|
||||
//Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection
|
||||
Log.d("LanLinkProvider", "Starting handshake");
|
||||
sslSocket.startHandshake();
|
||||
Log.d("LanLinkProvider", "Handshake done");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a link in the {@link #visibleComputers} map. This method is synchronized, which ensures that only one
|
||||
* link is operated on at a time.
|
||||
* <p>
|
||||
* Without synchronization, the call to {@link SslHelper#parseCertificate(byte[])} in
|
||||
* {@link Device#addLink(NetworkPacket, BaseLink)} crashes on some devices running Oreo 8.1 (SDK level 27).
|
||||
* </p>
|
||||
* 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 = visibleComputers.get(deviceId);
|
||||
if (currentLink != null) {
|
||||
//Update old link
|
||||
Log.i("KDE/LanLinkProvider", "Reusing same link for device " + deviceId);
|
||||
final Socket oldSocket = currentLink.reset(socket);
|
||||
//Log.e("KDE/LanLinkProvider", "Replacing socket. old: "+ oldSocket.hashCode() + " - new: "+ socket.hashCode());
|
||||
private LanLink addLink(SSLSocket socket, DeviceInfo deviceInfo) throws IOException {
|
||||
LanLink link = visibleDevices.get(deviceInfo.id);
|
||||
if (link != null) {
|
||||
// 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);
|
||||
visibleComputers.put(deviceId, link);
|
||||
connectionAccepted(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);
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
public LanLinkProvider(Context context) {
|
||||
this.context = context;
|
||||
this.mdnsDiscovery = new MdnsDiscovery(context, this);
|
||||
}
|
||||
|
||||
private void setupUdpListener() {
|
||||
@@ -296,14 +269,19 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
ThreadHelper.execute(() -> {
|
||||
Log.i("UdpListener", "Starting UDP listener");
|
||||
while (listening) {
|
||||
final int bufferSize = 1024 * 512;
|
||||
byte[] data = new byte[bufferSize];
|
||||
DatagramPacket packet = new DatagramPacket(data, bufferSize);
|
||||
try {
|
||||
DatagramPacket packet = new DatagramPacket(new byte[MAX_UDP_PACKET_SIZE], MAX_UDP_PACKET_SIZE);
|
||||
udpServer.receive(packet);
|
||||
udpPacketReceived(packet);
|
||||
} catch (Exception e) {
|
||||
ThreadHelper.execute(() -> {
|
||||
try {
|
||||
udpPacketReceived(packet);
|
||||
} catch (JSONException | IOException e) {
|
||||
Log.e("LanLinkProvider", "Exception receiving incoming UDP connection", e);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Log.e("LanLinkProvider", "UdpReceive exception", e);
|
||||
onNetworkChange(); // Trigger a UDP broadcast to try to get them to connect to us instead
|
||||
}
|
||||
}
|
||||
Log.w("UdpListener", "Stopping UDP listener");
|
||||
@@ -322,7 +300,13 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
try {
|
||||
Socket socket = tcpServer.accept();
|
||||
configureSocket(socket);
|
||||
tcpPacketReceived(socket);
|
||||
ThreadHelper.execute(() -> {
|
||||
try {
|
||||
tcpPacketReceived(socket);
|
||||
} catch (IOException e) {
|
||||
Log.e("LanLinkProvider", "Exception receiving incoming TCP connection", e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e("LanLinkProvider", "TcpReceive exception", e);
|
||||
}
|
||||
@@ -351,7 +335,13 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
throw new RuntimeException("This should not be reachable");
|
||||
}
|
||||
|
||||
private void broadcastUdpPacket() {
|
||||
private void broadcastUdpIdentityPacket() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (!preferences.getBoolean(SettingsFragment.KEY_UDP_BROADCAST_ENABLED, true)) {
|
||||
Log.i("LanLinkProvider", "UDP broadcast is disabled in settings. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() < lastBroadcast + delayBetweenBroadcasts) {
|
||||
Log.i("LanLinkProvider", "broadcastUdpPacket: relax cowboy");
|
||||
return;
|
||||
@@ -359,57 +349,73 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
lastBroadcast = System.currentTimeMillis();
|
||||
|
||||
ThreadHelper.execute(() -> {
|
||||
ArrayList<String> iplist = CustomDevicesActivity
|
||||
List<String> ipStringList = CustomDevicesActivity
|
||||
.getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context));
|
||||
|
||||
if (TrustedNetworkHelper.isTrustedNetwork(context)) {
|
||||
iplist.add("255.255.255.255"); //Default: broadcast.
|
||||
ipStringList.add("255.255.255.255"); //Default: broadcast.
|
||||
} else {
|
||||
Log.i("LanLinkProvider", "Current network isn't trusted, not broadcasting");
|
||||
}
|
||||
|
||||
if (iplist.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkPacket identity = NetworkPacket.createIdentityPacket(context);
|
||||
if (tcpServer == null || !tcpServer.isBound()) {
|
||||
Log.i("LanLinkProvider", "Won't broadcast UDP packet if TCP socket is not ready yet");
|
||||
return;
|
||||
}
|
||||
int port = tcpServer.getLocalPort();
|
||||
identity.set("tcpPort", port);
|
||||
DatagramSocket socket = null;
|
||||
byte[] bytes = null;
|
||||
try {
|
||||
socket = new DatagramSocket();
|
||||
socket.setReuseAddress(true);
|
||||
socket.setBroadcast(true);
|
||||
bytes = identity.serialize().getBytes(Charsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
Log.e("KDE/LanLinkProvider", "Failed to create DatagramSocket", e);
|
||||
}
|
||||
|
||||
if (bytes != null) {
|
||||
Log.i("KDE/LanLinkProvider","Sending broadcast to "+iplist.size()+" ips");
|
||||
for (String ipstr : iplist) {
|
||||
try {
|
||||
InetAddress client = InetAddress.getByName(ipstr);
|
||||
socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT));
|
||||
//Log.i("KDE/LanLinkProvider","Udp identity packet sent to address "+client);
|
||||
} catch (Exception e) {
|
||||
Log.e("KDE/LanLinkProvider", "Sending udp identity packet failed. Invalid address? (" + ipstr + ")", e);
|
||||
}
|
||||
ArrayList<InetAddress> ipList = new ArrayList<>();
|
||||
for (String ip : ipStringList) {
|
||||
try {
|
||||
ipList.add(InetAddress.getByName(ip));
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
if (ipList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendUdpIdentityPacket(ipList);
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public void sendUdpIdentityPacket(List<InetAddress> ipList) {
|
||||
if (tcpServer == null || !tcpServer.isBound()) {
|
||||
Log.i("LanLinkProvider", "Won't broadcast UDP packet if TCP socket is not ready yet");
|
||||
return;
|
||||
}
|
||||
|
||||
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
|
||||
NetworkPacket identity = myDeviceInfo.toIdentityPacket();
|
||||
identity.set("tcpPort", tcpServer.getLocalPort());
|
||||
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = identity.serialize().getBytes(Charsets.UTF_8);
|
||||
} catch (JSONException e) {
|
||||
Log.e("KDE/LanLinkProvider", "Failed to serialize identity packet", e);
|
||||
return;
|
||||
}
|
||||
|
||||
DatagramSocket socket;
|
||||
try {
|
||||
socket = new DatagramSocket();
|
||||
socket.setReuseAddress(true);
|
||||
socket.setBroadcast(true);
|
||||
} catch (SocketException e) {
|
||||
Log.e("KDE/LanLinkProvider", "Failed to create DatagramSocket", e);
|
||||
return;
|
||||
}
|
||||
|
||||
for (InetAddress ip : ipList) {
|
||||
try {
|
||||
socket.send(new DatagramPacket(bytes, bytes.length, ip, MIN_PORT));
|
||||
//Log.i("KDE/LanLinkProvider","Udp identity packet sent to address "+client);
|
||||
} catch (IOException e) {
|
||||
Log.e("KDE/LanLinkProvider", "Sending udp identity packet failed. Invalid address? (" + ip.toString() + ")", e);
|
||||
}
|
||||
}
|
||||
|
||||
socket.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
//Log.i("KDE/LanLinkProvider", "onStart");
|
||||
@@ -420,13 +426,18 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
setupUdpListener();
|
||||
setupTcpListener();
|
||||
|
||||
broadcastUdpPacket();
|
||||
mdnsDiscovery.startListening();
|
||||
mdnsDiscovery.startAnnouncing();
|
||||
|
||||
broadcastUdpIdentityPacket();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkChange() {
|
||||
broadcastUdpPacket();
|
||||
broadcastUdpIdentityPacket();
|
||||
mdnsDiscovery.stopListening();
|
||||
mdnsDiscovery.startListening();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -443,6 +454,8 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
} catch (Exception e) {
|
||||
Log.e("LanLink", "Exception", e);
|
||||
}
|
||||
mdnsDiscovery.stopAnnouncing();
|
||||
mdnsDiscovery.stopListening();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
206
src/org/kde/kdeconnect/Backends/LanBackend/MdnsDiscovery.java
Normal file
206
src/org/kde/kdeconnect/Backends/LanBackend/MdnsDiscovery.java
Normal file
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* 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.Backends.LanBackend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.nsd.NsdManager;
|
||||
import android.net.nsd.NsdServiceInfo;
|
||||
import android.util.Log;
|
||||
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collections;
|
||||
|
||||
public class MdnsDiscovery {
|
||||
|
||||
static final String LOG_TAG = "MdnsDiscovery";
|
||||
|
||||
static final String SERVICE_TYPE = "_kdeconnect._udp";
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final LanLinkProvider lanLinkProvider;
|
||||
|
||||
private final NsdManager mNsdManager;
|
||||
private NsdManager.RegistrationListener registrationListener;
|
||||
private NsdManager.DiscoveryListener discoveryListener;
|
||||
|
||||
public MdnsDiscovery(Context context, LanLinkProvider lanLinkProvider) {
|
||||
this.context = context;
|
||||
this.lanLinkProvider = lanLinkProvider;
|
||||
mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
|
||||
}
|
||||
|
||||
void startListening() {
|
||||
if (discoveryListener == null) {
|
||||
discoveryListener = createDiscoveryListener();
|
||||
mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
|
||||
}
|
||||
}
|
||||
|
||||
void stopListening() {
|
||||
if (discoveryListener != null) {
|
||||
mNsdManager.stopServiceDiscovery(discoveryListener);
|
||||
discoveryListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
void stopAnnouncing() {
|
||||
if (registrationListener != null) {
|
||||
mNsdManager.unregisterService(registrationListener);
|
||||
registrationListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
void startAnnouncing() {
|
||||
if (registrationListener == null) {
|
||||
registrationListener = createRegistrationListener();
|
||||
NsdServiceInfo serviceInfo = createNsdServiceInfo();
|
||||
mNsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
|
||||
}
|
||||
}
|
||||
|
||||
NsdManager.RegistrationListener createRegistrationListener() {
|
||||
return new NsdManager.RegistrationListener() {
|
||||
|
||||
@Override
|
||||
public void onServiceRegistered(NsdServiceInfo serviceInfo) {
|
||||
// If Android changed the service name to avoid conflicts, here we can read it.
|
||||
Log.i(LOG_TAG, "Registered " + serviceInfo.getServiceName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
||||
Log.e(LOG_TAG, "Registration failed with: " + errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
|
||||
Log.d(LOG_TAG, "Service unregistered: " + serviceInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
||||
Log.e(LOG_TAG, "Unregister of " + serviceInfo + " failed with: " + errorCode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public NsdServiceInfo createNsdServiceInfo() {
|
||||
NsdServiceInfo serviceInfo = new NsdServiceInfo();
|
||||
|
||||
InetAddress address = lanLinkProvider.udpServer.getInetAddress();
|
||||
int port = lanLinkProvider.udpServer.getLocalPort();
|
||||
serviceInfo.setHost(address);
|
||||
serviceInfo.setPort(port);
|
||||
|
||||
// iOS seems to need these as a TXT records
|
||||
serviceInfo.setAttribute("ip", address.toString());
|
||||
serviceInfo.setAttribute("port", Integer.toString(port));
|
||||
|
||||
// The following fields aren't really used for anything, since we can't include enough info
|
||||
// for it to be useful (namely: we can't include the device certificate).
|
||||
// Each field (key + value) needs to be < 255 bytes. All the fields combined need to be < 1300 bytes.
|
||||
// Also, on Android Lollipop those fields aren't resolved.
|
||||
String deviceId = DeviceHelper.getDeviceId(context);
|
||||
String deviceName = DeviceHelper.getDeviceName(context);
|
||||
String deviceType = DeviceHelper.getDeviceType(context).toString();
|
||||
String protocolVersion = Integer.toString(DeviceHelper.ProtocolVersion);
|
||||
serviceInfo.setAttribute("id", deviceId);
|
||||
serviceInfo.setAttribute("name", deviceName);
|
||||
serviceInfo.setAttribute("type", deviceType);
|
||||
serviceInfo.setAttribute("version", protocolVersion);
|
||||
|
||||
// Without resolving the DNS, the service name is the only info we have so it must be sufficient to identify a device.
|
||||
// Also, it must be unique, otherwise it will be automatically renamed. For these reasons we use the deviceId.
|
||||
serviceInfo.setServiceName(deviceId);
|
||||
serviceInfo.setServiceType(SERVICE_TYPE);
|
||||
|
||||
Log.d(LOG_TAG, "My MDNS info: " + serviceInfo);
|
||||
|
||||
return serviceInfo;
|
||||
}
|
||||
|
||||
NsdManager.DiscoveryListener createDiscoveryListener() {
|
||||
return new NsdManager.DiscoveryListener() {
|
||||
|
||||
final String myId = DeviceHelper.getDeviceId(context);
|
||||
|
||||
@Override
|
||||
public void onDiscoveryStarted(String regType) {
|
||||
Log.i(LOG_TAG, "Service discovery started");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceFound(NsdServiceInfo serviceInfo) {
|
||||
Log.d(LOG_TAG, "Service discovered: " + serviceInfo);
|
||||
|
||||
String deviceId = serviceInfo.getServiceName();
|
||||
|
||||
if (myId.equals(deviceId)) {
|
||||
Log.d(LOG_TAG, "Discovered myself, ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (lanLinkProvider.visibleDevices.containsKey(deviceId)) {
|
||||
Log.i(LOG_TAG, "MDNS discovered " + deviceId + " to which I'm already connected to. Ignoring.");
|
||||
return;
|
||||
}
|
||||
mNsdManager.resolveService(serviceInfo, createResolveListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceLost(NsdServiceInfo serviceInfo) {
|
||||
Log.w(LOG_TAG, "Service lost: " + serviceInfo);
|
||||
// We can't see this device via mdns. This probably means it's not reachable anymore
|
||||
// but we do nothing here since we have other ways to do detect unreachable devices
|
||||
// that hopefully will also trigger.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDiscoveryStopped(String serviceType) {
|
||||
Log.i(LOG_TAG, "MDNS discovery stopped: " + serviceType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
|
||||
Log.e(LOG_TAG, "MDNS discovery start failed: " + errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
|
||||
Log.e(LOG_TAG, "MDNS discovery stop failed: " + errorCode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a new listener instance since NsdManager wants a different listener each time you call resolveService
|
||||
*/
|
||||
NsdManager.ResolveListener createResolveListener() {
|
||||
return new NsdManager.ResolveListener() {
|
||||
@Override
|
||||
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
||||
Log.w(LOG_TAG, "MDNS error " + errorCode + " resolving service: " + serviceInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceResolved(NsdServiceInfo serviceInfo) {
|
||||
Log.i(LOG_TAG, "MDNS successfully resolved " + serviceInfo);
|
||||
|
||||
InetAddress remoteAddress = serviceInfo.getHost();
|
||||
|
||||
// Let the LanLinkProvider handle the connection
|
||||
lanLinkProvider.sendUdpIdentityPacket(Collections.singletonList(remoteAddress));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
connectionAccepted(deviceId, SslHelper.certificate, np, new LoopbackLink(context, this));
|
||||
LoopbackLink link = new LoopbackLink(context, this);
|
||||
onConnectionReceived(link);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -64,6 +64,8 @@ public class BackgroundService extends Service {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static boolean initialized = false;
|
||||
|
||||
// This indicates when connected over wifi/usb/bluetooth/(anything other than cellular)
|
||||
private final MutableLiveData<Boolean> connectedToNonCellularNetwork = new MutableLiveData<>();
|
||||
public LiveData<Boolean> isConnectedToNonCellularNetwork() {
|
||||
@@ -88,6 +90,10 @@ public class BackgroundService extends Service {
|
||||
}
|
||||
|
||||
public void onNetworkChange() {
|
||||
if (!initialized) {
|
||||
Log.d("KDE/BackgroundService", "ignoring onNetworkChange called before the service is initialized");
|
||||
return;
|
||||
}
|
||||
Log.d("KDE/BackgroundService", "onNetworkChange");
|
||||
for (BaseLinkProvider a : linkProviders) {
|
||||
a.onNetworkChange();
|
||||
@@ -145,6 +151,7 @@ public class BackgroundService extends Service {
|
||||
for (BaseLinkProvider a : linkProviders) {
|
||||
a.onStart();
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
private static NetworkRequest.Builder getNonCellularNetworkRequestBuilder() {
|
||||
@@ -248,6 +255,7 @@ public class BackgroundService extends Service {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.d("KdeConnect/BgService", "onDestroy");
|
||||
initialized = false;
|
||||
for (BaseLinkProvider a : linkProviders) {
|
||||
a.onStop();
|
||||
}
|
||||
|
@@ -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;
|
||||
@@ -34,18 +32,12 @@ import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.UserInterface.MainActivity;
|
||||
import org.kde.kdeconnect.UserInterface.PairingHandler;
|
||||
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;
|
||||
@@ -54,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,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() {
|
||||
@@ -196,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,20 +189,11 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
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();
|
||||
|
||||
@@ -291,9 +213,9 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
@Override
|
||||
public void unpaired() {
|
||||
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) {
|
||||
@@ -335,7 +257,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()))
|
||||
@@ -366,7 +288,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);
|
||||
}
|
||||
@@ -374,33 +296,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) {
|
||||
@@ -418,6 +318,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) {
|
||||
|
||||
@@ -581,7 +505,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;
|
||||
@@ -616,16 +540,16 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
if (existing != null) {
|
||||
|
||||
if (!existing.isCompatible()) {
|
||||
Log.i("KDE/addPlugin", "Minimum requirements (e.g. API level) not fulfilled " + pluginKey);
|
||||
Log.d("KDE/addPlugin", "Minimum requirements (e.g. API level) not fulfilled " + pluginKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Log.w("KDE/addPlugin","plugin already present:" + pluginKey);
|
||||
if (existing.checkOptionalPermissions()) {
|
||||
Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey);
|
||||
Log.d("KDE/addPlugin", "Optional Permissions OK " + pluginKey);
|
||||
pluginsWithoutOptionalPermissions.remove(pluginKey);
|
||||
} else {
|
||||
Log.e("KDE/addPlugin", "No optional permission " + pluginKey);
|
||||
Log.d("KDE/addPlugin", "No optional permission " + pluginKey);
|
||||
pluginsWithoutOptionalPermissions.put(pluginKey, existing);
|
||||
}
|
||||
return true;
|
||||
@@ -638,25 +562,25 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
}
|
||||
|
||||
if (!plugin.isCompatible()) {
|
||||
Log.i("KDE/addPlugin", "Minimum requirements (e.g. API level) not fulfilled " + pluginKey);
|
||||
Log.d("KDE/addPlugin", "Minimum requirements (e.g. API level) not fulfilled " + pluginKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
plugins.put(pluginKey, plugin);
|
||||
|
||||
if (!plugin.checkRequiredPermissions()) {
|
||||
Log.e("KDE/addPlugin", "No permission " + pluginKey);
|
||||
Log.d("KDE/addPlugin", "No permission " + pluginKey);
|
||||
plugins.remove(pluginKey);
|
||||
pluginsWithoutPermissions.put(pluginKey, plugin);
|
||||
return false;
|
||||
} else {
|
||||
Log.i("KDE/addPlugin", "Permissions OK " + pluginKey);
|
||||
Log.d("KDE/addPlugin", "Permissions OK " + pluginKey);
|
||||
pluginsWithoutPermissions.remove(pluginKey);
|
||||
if (plugin.checkOptionalPermissions()) {
|
||||
Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey);
|
||||
Log.d("KDE/addPlugin", "Optional Permissions OK " + pluginKey);
|
||||
pluginsWithoutOptionalPermissions.remove(pluginKey);
|
||||
} else {
|
||||
Log.e("KDE/addPlugin", "No optional permission " + pluginKey);
|
||||
Log.d("KDE/addPlugin", "No optional permission " + pluginKey);
|
||||
pluginsWithoutOptionalPermissions.put(pluginKey, plugin);
|
||||
}
|
||||
}
|
||||
@@ -698,6 +622,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,7 +19,10 @@ import android.util.Log;
|
||||
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 +56,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,4 +149,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;
|
||||
|
||||
@@ -22,11 +22,8 @@ import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.UserInterface.PairingHandler;
|
||||
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;
|
||||
|
||||
@@ -77,6 +74,7 @@ public class KdeConnect extends Application {
|
||||
}
|
||||
|
||||
private void onDeviceListChanged() {
|
||||
Log.i("MainActivity","Device list changed, notifying "+ deviceListChangedCallbacks.size() +" observers.");
|
||||
for (DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) {
|
||||
callback.onDeviceListChanged();
|
||||
}
|
||||
@@ -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();
|
||||
@@ -170,21 +158,22 @@ public class KdeConnect extends Application {
|
||||
|
||||
@Override
|
||||
public void onConnectionLost(BaseLink link) {
|
||||
Device d = devices.get(link.getDeviceId());
|
||||
Device device = devices.get(link.getDeviceId());
|
||||
Log.i("KDE/onConnectionLost", "removeLink, deviceId: " + link.getDeviceId());
|
||||
if (d != null) {
|
||||
d.removeLink(link);
|
||||
if (!d.isReachable() && !d.isPaired()) {
|
||||
if (device != null) {
|
||||
device.removeLink(link);
|
||||
if (!device.isReachable() && !device.isPaired()) {
|
||||
//Log.e("onConnectionLost","Removing connection device because it was not paired");
|
||||
devices.remove(link.getDeviceId());
|
||||
d.removePairingCallback(devicePairingCallback);
|
||||
device.removePairingCallback(devicePairingCallback);
|
||||
}
|
||||
} else {
|
||||
//Log.d("KDE/onConnectionLost","Removing connection to unknown device");
|
||||
Log.d("KDE/onConnectionLost","Removing connection to unknown device");
|
||||
}
|
||||
onDeviceListChanged();
|
||||
}
|
||||
};
|
||||
|
||||
public BaseLinkProvider.ConnectionReceiver getConnectionListener() {
|
||||
return connectionListener;
|
||||
}
|
||||
|
@@ -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;
|
||||
@@ -167,7 +162,7 @@ public class NetworkPacket {
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getStringSet(String key) {
|
||||
public Set<String> getStringSet(String key) {
|
||||
JSONArray jsonArray = mBody.optJSONArray(key);
|
||||
if (jsonArray == null) return null;
|
||||
Set<String> list = new HashSet<>();
|
||||
@@ -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() {
|
||||
|
@@ -4,12 +4,10 @@
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.UserInterface;
|
||||
package org.kde.kdeconnect;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.Timer;
|
||||
@@ -62,10 +60,15 @@ public class PairingHandler {
|
||||
Log.w("PairingHandler", "Ignoring second pairing request before the first one timed out");
|
||||
break;
|
||||
case Paired:
|
||||
Log.w("PairingHandler", "Auto-accepting pairing request from a device we already trusted");
|
||||
acceptPairing();
|
||||
break;
|
||||
case NotPaired:
|
||||
if (mPairState == PairState.Paired) {
|
||||
Log.w("PairingHandler", "Received pairing request from a device we already trusted.");
|
||||
// It would be nice to auto-accept the pairing request here, but since the pairing accept and pairing request
|
||||
// messages are identical, this could create an infinite loop if both devices are "accepting" each other pairs.
|
||||
// Instead, unpair and handle as if "NotPaired".
|
||||
mPairState = PairState.NotPaired;
|
||||
mCallback.unpaired();
|
||||
}
|
||||
mPairState = PairState.RequestedByPeer;
|
||||
|
||||
mPairingTimer = new Timer();
|
||||
@@ -89,13 +92,14 @@ public class PairingHandler {
|
||||
break;
|
||||
case Requested: // We started pairing and got rejected
|
||||
case RequestedByPeer: // They stared pairing, then cancelled
|
||||
mPairState = PairState.NotPaired;
|
||||
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer));
|
||||
break;
|
||||
case Paired:
|
||||
mPairState = PairState.NotPaired;
|
||||
mCallback.unpaired();
|
||||
break;
|
||||
}
|
||||
mPairState = PairState.NotPaired;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,9 +195,11 @@ public class PairingHandler {
|
||||
|
||||
public void unpair() {
|
||||
mPairState = PairState.NotPaired;
|
||||
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
|
||||
np.set("pair", false);
|
||||
mDevice.sendPacket(np);
|
||||
if (mDevice.isReachable()) {
|
||||
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
|
||||
np.set("pair", false);
|
||||
mDevice.sendPacket(np);
|
||||
}
|
||||
mCallback.unpaired();
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -168,7 +168,7 @@ public class PluginFactory {
|
||||
//Check incoming against outgoing
|
||||
if (Collections.disjoint(outgoing, info.getSupportedPacketTypes())
|
||||
&& Collections.disjoint(incoming, info.getOutgoingPacketTypes())) {
|
||||
Log.i("PluginFactory", "Won't load " + pluginId + " because of unmatched capabilities");
|
||||
Log.d("PluginFactory", "Won't load " + pluginId + " because of unmatched capabilities");
|
||||
continue; //No capabilities in common, do not load this plugin
|
||||
}
|
||||
plugins.add(pluginId);
|
||||
|
@@ -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);
|
||||
|
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2014 The Android Open Source Project
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.kde.kdeconnect.UserInterface;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
/**
|
||||
* A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
|
||||
* to be used with AppCompat.
|
||||
* <p>
|
||||
* This technique can be used with an {@link android.app.Activity} class, not just
|
||||
* {@link android.preference.PreferenceActivity}.
|
||||
*/
|
||||
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
||||
|
||||
private AppCompatDelegate mDelegate;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceState);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
getDelegate().onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
public ActionBar getSupportActionBar() {
|
||||
return getDelegate().getSupportActionBar();
|
||||
}
|
||||
|
||||
public void setSupportActionBar(@Nullable Toolbar toolbar) {
|
||||
getDelegate().setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MenuInflater getMenuInflater() {
|
||||
return getDelegate().getMenuInflater();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(@LayoutRes int layoutResID) {
|
||||
getDelegate().setContentView(layoutResID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view) {
|
||||
getDelegate().setContentView(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view, ViewGroup.LayoutParams params) {
|
||||
getDelegate().setContentView(view, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addContentView(View view, ViewGroup.LayoutParams params) {
|
||||
getDelegate().addContentView(view, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
getDelegate().onPostResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTitleChanged(CharSequence title, int color) {
|
||||
super.onTitleChanged(title, color);
|
||||
getDelegate().setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
getDelegate().onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
getDelegate().onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateOptionsMenu() {
|
||||
getDelegate().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
private AppCompatDelegate getDelegate() {
|
||||
if (mDelegate == null) {
|
||||
mDelegate = AppCompatDelegate.create(this, null);
|
||||
}
|
||||
return mDelegate;
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -34,6 +34,7 @@ import org.kde.kdeconnect.Device
|
||||
import org.kde.kdeconnect.Device.PluginsChangedListener
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
|
||||
import org.kde.kdeconnect.KdeConnect
|
||||
import org.kde.kdeconnect.PairingHandler
|
||||
import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin
|
||||
import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin
|
||||
import org.kde.kdeconnect.Plugins.Plugin
|
||||
|
@@ -37,6 +37,9 @@ import org.kde.kdeconnect_tp.R;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
public static final String KEY_UDP_BROADCAST_ENABLED = "udp_broadcast_enabled";
|
||||
public static final String KEY_APP_THEME = "theme_pref";
|
||||
|
||||
private EditTextPreference renameDevice;
|
||||
|
||||
@NonNull
|
||||
@@ -90,7 +93,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
// Theme Selector
|
||||
ListPreference themeSelector = new ListPreference(context);
|
||||
themeSelector.setKey("theme_pref");
|
||||
themeSelector.setKey(KEY_APP_THEME);
|
||||
themeSelector.setTitle(R.string.theme_dialog_title);
|
||||
themeSelector.setDialogTitle(R.string.theme_dialog_title);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -168,6 +171,13 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
return true;
|
||||
});
|
||||
|
||||
// UDP broadcast toggle
|
||||
final TwoStatePreference udpBroadcastDiscovery = new SwitchPreference(context);
|
||||
udpBroadcastDiscovery.setPersistent(false);
|
||||
udpBroadcastDiscovery.setDefaultValue(true);
|
||||
udpBroadcastDiscovery.setKey(KEY_UDP_BROADCAST_ENABLED);
|
||||
udpBroadcastDiscovery.setTitle(R.string.enable_udp_broadcast);
|
||||
screen.addPreference(udpBroadcastDiscovery);
|
||||
|
||||
// More settings text
|
||||
Preference moreSettingsText = new Preference(context);
|
||||
|
@@ -9,7 +9,6 @@ package org.kde.kdeconnect;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
@@ -33,7 +32,6 @@ import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.UserInterface.PairingHandler;
|
||||
import org.mockito.Mockito;
|
||||
import org.powermock.api.mockito.PowerMockito;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
@@ -41,10 +39,7 @@ import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@@ -89,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);
|
||||
@@ -115,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
|
||||
@@ -129,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 {
|
||||
@@ -142,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" +
|
||||
@@ -162,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