2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-09-02 23:25:10 +00:00

Compare commits

..

41 Commits

Author SHA1 Message Date
Albert Vaca Cintora
e2aa79a252 Release 1.32.3 2024-09-28 18:26:16 +02:00
Albert Vaca Cintora
1ffcaf076e Bump deps 2024-09-28 18:21:06 +02:00
TPJ Schikhof
854b2a1c9f Migrate PluginFactory to Kotlin 2024-09-28 04:59:56 +00:00
l10n daemon script
8fb545d620 GIT_SILENT made messages (after extraction) 2024-09-27 01:53:54 +00:00
Marko Zajc
db615b82df Fix trusted networks
## Summary
1) Fix the code responsible for loading the trusted network list.
2) The old `#_#` delimiter has been replaced with a less likely NUL character. This requires re-adding trusted networks, so I can revert it if necessary.
3) Ignore incoming identity packets on untrusted devices if they come from an untrusted device.

BUG: 492302

## Test Plan

### Before:
1) Trusted networks were completely broken, an would show variations of `#` and `_` on the list due to a bug in the splitting code.
2) Any network with `#_#` in the SSID - although unlikely - would not be possible to use as a trusted network.
3) The device was still discoverable on an untrusted network by manually refreshing the devices list.

### After:
1) Trusted networks now load the SSID list correctly.
2) Networks with `#_#` in the SSID can be added as trusted networks.
3) The device is no longer discoverable on an untrusted network.
2024-09-21 22:24:46 +00:00
TPJ Schikhof
ee7d4a1f05 Migrate FindRemoteDevicePlugin to Kotlin 2024-09-16 10:24:04 +00:00
TPJ Schikhof
409ef6b579 Migrate PingPlugin to Kotlin 2024-09-16 10:20:43 +00:00
TPJ Schikhof
2c0a9d262e Migrate MockSharedPreference to Kotlin 2024-09-16 10:18:16 +00:00
TPJ Schikhof
2f10f1d0f2 Migrate NsdResolveQueue to Kotlin 2024-09-15 08:07:31 +00:00
l10n daemon script
9f788da478 GIT_SILENT made messages (after extraction) 2024-09-12 01:45:57 +00:00
TPJ Schikhof
ec952b49d9 Migrate LoopbackBackend to Kotlin 2024-09-10 21:22:59 +00:00
TPJ Schikhof
45da75f331 Migrate BackgroundJob(Handler) to Kotlin 2024-09-10 08:35:09 +00:00
Albert Vaca Cintora
283956c107 Bump targetSdk from 33 to 35 2024-09-08 14:35:36 +02:00
l10n daemon script
d605977b91 GIT_SILENT Sync po/docbooks with svn 2024-09-06 02:42:47 +00:00
Albert Vaca Cintora
80cf238354 Add changelog 2024-09-03 12:10:28 +02:00
Albert Vaca Cintora
8ee135b018 Release 1.32.2 2024-09-03 12:05:55 +02:00
Albert Vaca Cintora
2343dbf144 Release 1.32.2 for Play Store without EXTERNAL STORAGE permission 2024-09-03 12:04:38 +02:00
Albert Vaca Cintora
3f53180b1d Unify the check for NativeFileSystem support 2024-09-03 12:03:03 +02:00
Albert Vaca Cintora
0f7af315f5 Bump deps 2024-09-03 12:03:03 +02:00
TPJ Schikhof
7ebac70d47 Migrate DeviceTest to Kotlin 2024-09-02 10:29:16 +00:00
TPJ Schikhof
9037647281 Migrate SafeTextCheckerTest to Kotlin 2024-09-02 10:27:24 +00:00
l10n daemon script
9373587268 GIT_SILENT made messages (after extraction) 2024-09-02 01:46:49 +00:00
TPJ Schikhof
0771415d8f Migrate RsaHelper to Kotlin 2024-08-31 17:38:59 +00:00
TPJ Schikhof
fb0e2cc01d Migrate files helper to Kotlin + add unit tests 2024-08-30 18:02:12 +00:00
TPJ Schikhof
584fb78bb7 Relieve getStorageList() out of it's misery since it's not being used anymore
I was about to migrate this to Kotlin but reasoned it would save some work to remove this first since it's not being used anyway.
2024-08-30 14:29:17 +00:00
l10n daemon script
ae12e3e6fc GIT_SILENT Sync po/docbooks with svn 2024-08-29 02:19:17 +00:00
l10n daemon script
8be80cae3e GIT_SILENT made messages (after extraction) 2024-08-29 01:47:24 +00:00
l10n daemon script
d41ed4d911 GIT_SILENT made messages (after extraction) 2024-08-28 01:57:12 +00:00
l10n daemon script
6f783b54a5 GIT_SILENT made messages (after extraction) 2024-08-27 01:50:35 +00:00
l10n daemon script
1f6d1ea544 GIT_SILENT Sync po/docbooks with svn 2024-08-26 02:33:49 +00:00
l10n daemon script
27737c46f2 GIT_SILENT made messages (after extraction) 2024-08-26 02:00:08 +00:00
l10n daemon script
6b99fe7e7c GIT_SILENT made messages (after extraction) 2024-08-25 01:47:57 +00:00
Adam Liscak
6e59a6241a Certificate Expiry Hadling After 10 Years
Auto reconfiguration of own certificate:

currently:
if kdeconncect loads its certificate and its expired or not effective yet
it generates a new certificate and deletes all remembered devices
(since these will stay unreachable anyways)

previously:
if kdeconncect loads its certificate and its expired or not effective yet
it continues having the same certificate
This brings forth an issue: Other devices would refuse to connect to a device with
an expired or non-effective certificate.

Auto-delete of orphan certificates:

currently:
Devices in kdeconnect's devicelist that have illegal ssl certificates
(expired, not effective yet, empty) get automatically deleted from the
devicelist
previously:
they would just exist forever until the user deletes them
2024-08-24 10:24:34 +00:00
l10n daemon script
7506d32cef GIT_SILENT made messages (after extraction) 2024-08-24 01:47:48 +00:00
l10n daemon script
c35dc4928c GIT_SILENT made messages (after extraction) 2024-08-23 01:47:03 +00:00
l10n daemon script
6070276489 GIT_SILENT Sync po/docbooks with svn 2024-08-22 02:30:41 +00:00
l10n daemon script
9685f7e69a GIT_SILENT made messages (after extraction) 2024-08-22 01:55:16 +00:00
TPJ Schikhof
69495136da Migrate RandomHelper to Kotlin 2024-08-21 18:55:00 +00:00
Mhammad Alloush
5f18cb571d Update MousePadActivity to support doubletap drag
This MR is a continuation of !439 , which changes the default 
behavior of the remote mouse plugin's drag and drop trigger
to be a double-tap to drag, and adds a new SwitchPreference
that toggles between the old (hold to drag) and new behavior.
2024-08-21 18:52:24 +00:00
TPJ Schikhof
ab1b1f7ecc Migrate MediaStoreHelper to Kotlin 2024-08-21 15:06:55 +00:00
l10n daemon script
3f8170eae8 GIT_SILENT made messages (after extraction) 2024-08-20 02:02:39 +00:00
83 changed files with 1627 additions and 1880 deletions

View File

@@ -8,8 +8,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:versionCode="13201"
android:versionName="1.32.1">
android:versionCode="13203"
android:versionName="1.32.3">
<uses-feature
android:name="android.hardware.telephony"

View File

@@ -51,7 +51,7 @@ android {
compileSdk = 34
defaultConfig {
minSdk = 21
targetSdk = 33
targetSdk = 35
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
buildFeatures {

View File

@@ -0,0 +1,11 @@
1.32.2
* Handle expired certificates
* Support doubletap drag in remote mouse
1.32.1
* Fixed a crash when opening the presentation remote
1.32
* Rewrite the remote file browsing
* Add Direct Share targets
* Send album art from phone to PC

View File

@@ -0,0 +1,14 @@
1.32.3
* Fix trusted devices list
1.32.2
* Handle expired certificates
* Support doubletap drag in remote mouse
1.32.1
* Fixed a crash when opening the presentation remote
1.32
* Rewrite the remote file browsing
* Add Direct Share targets
* Send album art from phone to PC

View File

@@ -6,7 +6,7 @@ KDEConnect fournit un ensemble de fonctionnalités pour intégrer votre flux de
- Apparition de notifications pour les appels et les messages entrants sur votre ordinateur.
- Pavé tactile virtuel : utilisation de l'écran de votre téléphone comme pavé tactile pour votre ordinateur.
- Synchronisation de vos notifications : accès à vos notifications téléphoniques depuis votre ordinateur et réponses aux messages.
- Télé-commande multimédia : utilisation de votre téléphone comme télécommande pour les lecteurs de médias sous Linux.
- Télé-commande multimédia : utilisation de votre téléphone comme télécommande pour les lecteurs de média sous Linux.
- Connexion au Wifi : aucun connexion USB ou Bluetooth nécessaire.
- Chiffrement « TLS » de bout en bout : vos informations sont en sécurité.
@@ -18,4 +18,4 @@ Informations sur les permissions sensibles :
KDEConnect n'envoie jamais d'informations à KDE ni à aucun tiers. KDEConnect envoie des données d'un périphérique à un autre à l'aide du réseau local, mais jamais par Internet et en utilisant le chiffrement de bout en bout.
Cette application fait partie d'un projet « Open source ». Il existe grâce à toutes les personnes qui y ont contribué.Visitez le site Internet pour accéder au code source.
Cette application fait partie d'un projet « Open source ». Il existe grâce à toutes les personnes qui y ont contribué. Veuillez visiter le site Internet pour accéder au code source.

View File

@@ -1,20 +1,21 @@
O KDE Connect fornece um conjunto de recursos para integrar seu fluxo de trabalho entre dispositivos:
- Transfira arquivos entre seus dispositivos.
- Acesse arquivos do seu computador no seu telefone, sem fios.
- Área de transferência compartilhada: copie e cole entre seus dispositivos.
- Compartilhe arquivos e URLs em seu computador a partir de qualquer app.
- Receba notificações de chamadas recebidas e mensagens SMS no seu PC.
- Touchpad virtual: use a tela do telefone como touchpad do computador.
- Sincronização de notificações: leia as notificações do seu Android na área de trabalho.
- Controle remoto multimídia: use seu telefone como controle remoto para reprodutores de mídia Linux.
- Sincronização de notificações: acesse as notificações do seu telefone no seu computador e responda as mensagens.
- Controle remoto multimídia: use seu telefone como controle remoto para reprodutores de mídia no Linux.
- Conexão Wi-Fi: sem necessidade de cabos USB ou bluetooth.
- Criptografia TLS de ponta a ponta: suas informações estão seguras.
Observe que você precisará instalar o KDE Connect no seu computador para que este aplicativo funcione e mantenha a versão para desktop atualizada com a versão do Android para que os recursos mais recentes funcionem.
Informações a respeito de permissões especiais :
Informações sobre permissões sensíveis:
* Permissão de acessibilidade: necessária para receber entrada de outro dispositivo para controlar seu telefone Android, se você usar o recurso de entrada remota.
* Permissão de localização em segundo plano: necessária para saber a qual rede Wi-Fi você está conectado, se você usar o recurso de redes confiáveis.
O KDE Connect nunca envia nenhuma informação ao KDE nem a terceiros. O KDE Connect envia dados de um dispositivo para outro diretamente usando a rede local, nunca pela Internet e usando criptografia de ponta a ponta.
O KDE Connect nunca envia nenhuma informação ao KDE ou a terceiros. O KDE Connect envia dados de um dispositivo para outro diretamente usando a rede local, nunca pela Internet e usando criptografia de ponta a ponta.
Este aplicativo faz parte de um projeto de código aberto e existe graças a todas as pessoas que contribuíram para ele. Visite o site para obter o código-fonte.

View File

@@ -1,14 +1,14 @@
[versions]
activityCompose = "1.9.1"
androidDesugarJdkLibs = "2.0.4"
androidGradlePlugin = "8.5.2"
activityCompose = "1.9.2"
androidDesugarJdkLibs = "2.1.2"
androidGradlePlugin = "8.6.1"
androidSmsmms = "kdeconnect-1-21-0"
appcompat = "1.7.0"
bcpkixJdk15on = "1.70"
classindex = "3.13"
commonsCollections4 = "4.4"
commonsIo = "2.16.1"
commonsLang3 = "3.16.0"
commonsIo = "2.17.0"
commonsLang3 = "3.17.0"
constraintlayoutCompose = "1.0.1"
coreKtx = "1.13.1"
dependencyLicenseReport = "2.7"
@@ -17,16 +17,16 @@ documentfile = "1.0.1"
gridlayout = "1.0.0"
jsonassert = "1.5.3"
junit = "4.13.2"
kotlin = "2.0.10"
kotlinxCoroutinesCore = "1.8.1"
kotlin = "2.0.20"
kotlinxCoroutinesCore = "1.9.0"
lifecycleExtensions = "2.2.0"
lifecycleRuntimeKtx = "2.8.4"
lifecycleRuntimeKtx = "2.8.6"
logger = "1.0.3"
material = "1.12.0"
material3 = "1.2.1"
material3 = "1.3.0"
media = "1.7.0"
minaCore = "2.2.3"
mockitoCore = "5.12.0"
mockitoCore = "5.14.0"
preferenceKtx = "1.2.1"
reactiveStreams = "1.0.4"
recyclerview = "1.3.2"
@@ -34,7 +34,7 @@ rxjava = "2.2.21"
sl4j = "2.0.13"
sshdCore = "2.13.2"
swiperefreshlayout = "1.1.0"
uiToolingPreview = "1.6.8"
uiToolingPreview = "1.7.2"
univocityParsers = "2.9.1"
[libraries]

View File

@@ -2,7 +2,7 @@
#. extracted from ./metadata/android/en-US/full_description.txt
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Project-Id-Version: kdeconnect-android-store-full\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-11-05 12:31+0000\n"
"PO-Revision-Date: 2023-06-07 19:50+0200\n"

View File

@@ -2,7 +2,7 @@
#. extracted from ./metadata/android/en-US/short_description.txt
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Project-Id-Version: kdeconnect-android-store-short\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-11-05 12:31+0000\n"
"PO-Revision-Date: 2023-06-07 19:50+0200\n"

View File

@@ -1,20 +1,20 @@
#
# SPDX-FileCopyrightText: 2023 Xavier Besnard <xavier.besnard@kde.org>
# SPDX-FileCopyrightText: 2023, 2024 Xavier Besnard <xavier.besnard@kde.org>
#. extracted from ./metadata/android/en-US/full_description.txt
msgid ""
msgstr ""
"Project-Id-Version: kdeconnect-android-store-full\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-11-05 12:31+0000\n"
"PO-Revision-Date: 2023-09-28 18:06+0200\n"
"Last-Translator: Xavier BESNARD <xavier.besnard@neuf.fr>\n"
"Language-Team: French <kde-francophone@kde.org>\n"
"PO-Revision-Date: 2024-08-09 22:07+0200\n"
"Last-Translator: Xavier Besnard <xavier.besnard@kde.org>\n"
"Language-Team: French <French <kde-francophone@kde.org>>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Lokalize 23.08.1\n"
"X-Generator: Lokalize 23.08.5\n"
msgid ""
"KDE Connect provides a set of features to integrate your workflow across "
@@ -62,7 +62,7 @@ msgstr ""
"- Synchronisation de vos notifications : accès à vos notifications "
"téléphoniques depuis votre ordinateur et réponses aux messages.\n"
"- Télé-commande multimédia : utilisation de votre téléphone comme "
"télécommande pour les lecteurs de médias sous Linux.\n"
"télécommande pour les lecteurs de média sous Linux.\n"
"- Connexion au Wifi : aucun connexion USB ou Bluetooth nécessaire.\n"
"- Chiffrement « TLS » de bout en bout : vos informations sont en sécurité.\n"
"\n"
@@ -83,5 +83,5 @@ msgstr ""
"mais jamais par Internet et en utilisant le chiffrement de bout en bout.\n"
"\n"
"Cette application fait partie d'un projet « Open source ». Il existe grâce à "
"toutes les personnes qui y ont contribué.Visitez le site Internet pour "
"accéder au code source.\n"
"toutes les personnes qui y ont contribué. Veuillez visiter le site Internet "
"pour accéder au code source.\n"

View File

@@ -9,10 +9,10 @@ msgstr ""
"PO-Revision-Date: 2024-07-10 20:18+0200\n"
"Last-Translator: Karl Ove Hufthammer <karl@huftis.org>\n"
"Language-Team: Norwegian Nynorsk <l10n-no@lister.huftis.org>\n"
"Language: nn\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: nn\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Lokalize 24.05.1\n"
"X-Environment: kde\n"

View File

@@ -1,53 +1,21 @@
# Geraldo Simiao <geraldosimiao@fedoraproject.org>, 2023.
# Frederico Goncalves Guimaraes <frederico@teia.bio.br>, 2024.
#. extracted from ./metadata/android/en-US/full_description.txt
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-11-05 12:31+0000\n"
"PO-Revision-Date: 2023-08-04 01:33-0300\n"
"Last-Translator: Geraldo Simiao <geraldosimiao@fedoraproject.org>\n"
"PO-Revision-Date: 2024-08-28 17:37-0300\n"
"Last-Translator: Frederico Goncalves Guimaraes <frederico@teia.bio.br>\n"
"Language-Team: Brazilian Portuguese <kde-i18n-pt_BR@kde.org>\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Lokalize 23.04.3\n"
"X-Generator: Lokalize 22.12.3\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#, fuzzy
#| 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"
#| "Sensitive permissions information:\n"
#| "* Accessibility permission: Required to receive input from another device "
#| "to control your Android phone, if you use the Remote Input feature.\n"
#| "* Background location permission: Required to know to which WiFi network "
#| "you are connected to, if you use the Trusted Networks feature.\n"
#| "\n"
#| "KDE Connect never sends any information to KDE nor to any third party. "
#| "KDE Connect sends data from one device to the other directly using the "
#| "local network, never through the internet, and using end to end "
#| "encryption.\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.\n"
msgid ""
"KDE Connect provides a set of features to integrate your workflow across "
"devices:\n"
@@ -84,15 +52,16 @@ msgstr ""
"O KDE Connect fornece um conjunto de recursos para integrar seu fluxo de "
"trabalho entre dispositivos:\n"
"\n"
"- Transfira arquivos entre seus dispositivos.\n"
"- Acesse arquivos do seu computador no seu telefone, sem fios.\n"
"- Área de transferência compartilhada: copie e cole entre seus "
"dispositivos.\n"
"- Compartilhe arquivos e URLs em seu computador a partir de qualquer app.\n"
"- Receba notificações de chamadas recebidas e mensagens SMS no seu PC.\n"
"- Touchpad virtual: use a tela do telefone como touchpad do computador.\n"
"- Sincronização de notificações: leia as notificações do seu Android na área "
"de trabalho.\n"
"- Sincronização de notificações: acesse as notificações do seu telefone no "
"seu computador e responda as mensagens.\n"
"- Controle remoto multimídia: use seu telefone como controle remoto para "
"reprodutores de mídia Linux.\n"
"reprodutores de mídia no Linux.\n"
"- Conexão Wi-Fi: sem necessidade de cabos USB ou bluetooth.\n"
"- Criptografia TLS de ponta a ponta: suas informações estão seguras.\n"
"\n"
@@ -100,14 +69,14 @@ msgstr ""
"este aplicativo funcione e mantenha a versão para desktop atualizada com a "
"versão do Android para que os recursos mais recentes funcionem.\n"
"\n"
"Informações a respeito de permissões especiais :\n"
"Informações sobre permissões sensíveis:\n"
"* Permissão de acessibilidade: necessária para receber entrada de outro "
"dispositivo para controlar seu telefone Android, se você usar o recurso de "
"entrada remota.\n"
"* Permissão de localização em segundo plano: necessária para saber a qual "
"rede Wi-Fi você está conectado, se você usar o recurso de redes confiáveis.\n"
"\n"
"O KDE Connect nunca envia nenhuma informação ao KDE nem a terceiros. O KDE "
"O KDE Connect nunca envia nenhuma informação ao KDE ou a terceiros. O KDE "
"Connect envia dados de um dispositivo para outro diretamente usando a rede "
"local, nunca pela Internet e usando criptografia de ponta a ponta.\n"
"\n"

View File

@@ -84,9 +84,11 @@
<string name="error_canceled_by_other_peer">ألغاه ندّ آخر</string>
<string name="encryption_info_title">معلومات التّعمية</string>
<string name="pair_requested">طُلب الاقتران</string>
<string name="pair_succeeded">نجح الاقتران</string>
<string name="tap_to_open">اطرق لتفتح</string>
<string name="received_file_text">المس لفتح \'%1s\'</string>
<string name="tap_to_answer">المس للإجابة</string>
<string name="left_click">أرسل نقرة باليسار</string>
<string name="right_click">أرسل نقرة باليمين</string>
<string name="middle_click">أرسل نقرة بالوسط</string>
<string name="show_keyboard">أظهر لوحة المفاتيح</string>
@@ -113,6 +115,7 @@
<item>دقيقة واحدة</item>
<item>دقيقتان</item>
</string-array>
<string name="share_to">شارك مع…</string>
<string name="protocol_version_newer">يستخدم هذا الجهاز إصدار ميفاق أحدث</string>
<string name="plugin_settings_with_name">إعدادات %s</string>
<string name="invalid_device_name">اسم جهاز غير صالح</string>
@@ -145,8 +148,10 @@
<string name="no_file_browser">لا متصفّحات ملفّات مثبّتة.</string>
<string name="pref_plugin_telepathy">أرسل SMS</string>
<string name="pref_plugin_telepathy_desc">أرسل رسائل نصّيّة من سطح المكتب</string>
<string name="pref_plugin_telepathy_mms">أرسل رسالة قصيرة</string>
<string name="findmyphone_title">جِد جهازي</string>
<string name="findmyphone_title_tablet">جِد جهازي اللوحيّ</string>
<string name="findmyphone_title_tv">جِد تلفازي</string>
<string name="findmyphone_description">يرّن هذا الجهاز لتجده</string>
<string name="findmyphone_found">عثر عليه</string>
<string name="open">افتح</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Изпращане</string>
<string name="compose_send_title">Текстът е изпратен</string>
<string name="open_compose_send">Съставяне на текст</string>
<string name="double_tap_to_drag">Двойно докосване за влачене</string>
<string name="hold_to_drag">Задържане за влачене</string>
<string name="about_kde_about">&lt;h1&gt;За&lt;/h1&gt; &lt;p&gt;KDE е световна общност от софтуерни инженери, художници, писатели, преводачи и творци, които са отдадени на &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;свободното разработване на софтуер&lt;/a&gt;. KDE създава работната среда Plasma, стотици приложения и многобройните софтуерни библиотеки, които ги поддържат.&lt;/p&gt; &lt;p&gt;KDE е кооперативно предприятие: нито една отделна организация контролира насоките или продуктите му. Вместо това ние работим заедно, за да постигнем общата цел да създадем най-добрия свободен софтуер в света. Всеки е добре дошъл да се присъедини и да да допринесе&lt;/a&gt; за KDE, включително и вие.&lt;/p&gt; Посетете &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; за повече информация за общността на KDE и за софтуера, който създаваме.</string>
<string name="about_kde_report_bugs_or_wishes">" &lt;h1&gt;Докладвайте за грешки или желания&lt;/h1&gt; &lt;p&gt;Софтуерът винаги може да бъде подобрен и екипът на KDE е готов да го направи. Въпреки това вие - потребителят - трябва да да ни кажете, когато нещо не работи според очакванията или може да бъде направено по-добре.&lt;/p&gt; &lt;p&gt;KDE разполага със система за проследяване на грешки. Посетете &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; или използвайте бутона \"Докладване на грешка\" от екрана за програмата, за да съобщите за грешки.&lt;/p&gt; Ако имате предложение за подобрение, тогава можете да използвате системата за проследяване на грешки, за да регистрирате желанието си. Уверете се, че използвате тежестта, наречена \"Wishlist\"."</string>
<string name="about_kde_join_kde">&lt;h1&gt;Присъединете се към KDE&lt;/h1&gt; &lt;p&gt;Не е необходимо да сте софтуерен разработчик на софтуер, за да сте член на екипа на KDE. Можете да се присъедините към преводаческите екипи за вашия език. Можете да предоставяте графики, теми, звуци, и подобрена документация. Вие решавате!&lt;/p&gt; &lt;p&gt;Посетете &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; за информация относно някои проекти, в които можете да да участвате.&lt;/p&gt; Ако имате нужда от повече информация или документация, тогава посетете &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; ще ви предостави необходимото.</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Envia</string>
<string name="compose_send_title">Títol de l\'enviament</string>
<string name="open_compose_send">Redacta text</string>
<string name="double_tap_to_drag">Dos tocs per a arrossegar</string>
<string name="hold_to_drag">Mantenir premut per a arrossegar</string>
<string name="about_kde_about">&lt;h1&gt;Quant al&lt;/h1&gt; &lt;p&gt;El KDE és una comunitat mundial d\'enginyers, artistes, escriptors, traductors i creadors de programari compromesos amb el desenvolupament de &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;programari lliure&lt;/a&gt;. El KDE produeix l\'entorn d\'escriptori Plasma, centenars d\'aplicacions i moltes biblioteques de programari que els donen suport.&lt;/p&gt; &lt;p&gt;El KDE és una empresa en cooperativa: cap entitat controla la seva direcció o els productes. En el seu lloc, treballem junts per a aconseguir l\'objectiu comú de construir el millor programari lliure del món. Tothom hi és benvingut a &lt;a href=https://community.kde.org/Get_Involved&gt;unir-se i contribuir&lt;/a&gt; al KDE, inclosos vosaltres.&lt;/p&gt; Visiteu &lt;a href=https://www.kde.org/ca/&gt;https://www.kde.org/ca/&lt;/a&gt; per a obtenir més informació sobre la comunitat KDE i el programari que produïm.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Informeu dels errors o desitjos&lt;/h1&gt; &lt;p&gt;El programari sempre es pot millorar, i l\'equip del KDE està a punt per a fer-ho. No obstant això, l\'usuari, ha de dir-nos quan alguna cosa no funciona com s\'esperava o si podria fer-se millor.&lt;/p&gt; &lt;p&gt;El KDE té un sistema de seguiment d\'errors. Per a informar-ne d\'un, visiteu &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; o useu el botó «Informeu d\'un error» des de la pantalla Quant al.&lt;/p&gt; Si teniu un suggeriment de millora, podeu usar el sistema de seguiment d\'errors per a enregistrar el vostre desig. Assegureu-vos d\'usar la severitat anomenada «Llista de desitjos» (Wishlist).</string>
<string name="about_kde_join_kde">&lt;h1&gt;Uniu-vos al KDE&lt;/h1&gt; &lt;p&gt;No cal ser un desenvolupador de programari per a ser membre de l\'equip KDE. Podeu unir-vos als equips d\'idiomes que tradueixen la interfície dels programes. Podeu proporcionar gràfics, temes, sons i documentació millorada. Vosaltres decidiu!&lt;/p&gt; &lt;p&gt;Visiteu &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; per a obtenir informació sobre alguns projectes en què podeu participar-hi.&lt;/p&gt; Si us cal més informació o documentació, una visita a &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; us proporcionarà el que necessiteu.</string>

View File

@@ -401,6 +401,8 @@
<string name="clear_compose">Vyprázdnit</string>
<string name="send_compose">Odeslat</string>
<string name="open_compose_send">Napsat text</string>
<string name="double_tap_to_drag">Přetáhnout dvojitým ťuknutím</string>
<string name="hold_to_drag">Přetáhnout podržením</string>
<string name="about_kde_about">&lt;h1&gt;O KDE&lt;/h1&gt; &lt;p&gt;KDE je celosvětová komunita softwarových inženýrů, výtvarníků, překladatelů a jiných přispěvatelů, kteří se odevzdali vývoji &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Svobodného Softwaru&lt;/a&gt;. KDE vytvořilo pracovní prostředí Plasma, stovky aplikací a spousty knihoven, jenž je podporují. &lt;/p&gt; &lt;p&gt;KDE je společné úsilí, kde žádná společnost neřídí jeho směr nebo produkty. Namísto toho spolupracujeme na společném cíli jímž je vytvoření nejlepšího Free Softwaru. Každý je vítán aby &lt;a href=https://community.kde.org/Get_Involved&gt;se zapojil a přispíval&lt;/a&gt; do KDE, včetně vás. Více informací o komunitě KDE a softwaru, na kterém pracujeme najdete na &lt;/a&gt;$3&lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt;.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Hlaste chyby a návrhy&lt;/h1&gt; &lt;p&gt;Software je možno neustále vylepšovat a tým KDE je k tomu připraven. Avšak vy, uživatel, nám musíte sdělit, když něco nefunguje tak, jak by se očekávalo nebo by mělo být uděláno lépe.&lt;/p&gt; &lt;p&gt;KDE má systém sledování chyb. Chcete-li tedy nahlásit chybu, navštivte &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; nebo použijte dialog \"Nahlásit chybu...\".&lt;/p&gt; Máte-li náměty na vylepšení, budeme rádi, pošlete-li nám svoje přání. Ujistěte se však, že jste označili chybové hlášení jako \"Přání\".</string>
<string name="about_kde_join_kde">&lt;h1&gt;Přidejte se ke KDE&lt;/h1&gt; &lt;p&gt;K tomu, abyste se stali členem týmu KDE, není zapotřebí být vývojářem softwaru. Můžete se připojit k překladatelským týmům, které překládají programy. Můžete vytvářet grafiku, motivy, zvuky a lepší dokumentaci. Vy se rozhodněte!&lt;/p&gt; &lt;p&gt;Navštivte &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; kde naleznete informace o některých projektech, kterých se můžete zúčastnit.&lt;/p&gt; Potřebujete-li více informací nebo dokumentace, pak návštěva na &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; vám poskytne, co potřebujete.&lt;/p&gt;České stránky o KDE se nacházejí na adrese &lt;a href=\"http://czechia.kde.org/\"&gt;http://czechia.kde.org/&lt;/a&gt; . Přidejte se k nám!</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Send</string>
<string name="compose_send_title">Compose send</string>
<string name="open_compose_send">Compose text</string>
<string name="double_tap_to_drag">Double tap to drag</string>
<string name="hold_to_drag">Hold to drag</string>
<string name="about_kde_about">&lt;h1&gt;About&lt;/h1&gt; &lt;p&gt;KDE is a world-wide community of software engineers, artists, writers, translators and creators who are committed to &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Free Software&lt;/a&gt; development. KDE produces the Plasma desktop environment, hundreds of applications, and the many software libraries that support them.&lt;/p&gt; &lt;p&gt;KDE is a cooperative enterprise: no single entity controls its direction or products. Instead, we work together to achieve the common goal of building the world\'s finest Free Software. Everyone is welcome to &lt;a href=https://community.kde.org/Get_Involved&gt;join and contribute&lt;/a&gt; to KDE, including you.&lt;/p&gt; Visit &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; for more information about the KDE community and the software we produce.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Report Bugs or Wishes&lt;/h1&gt; &lt;p&gt;Software can always be improved, and the KDE team is ready to do so. However, you - the user - must tell us when something does not work as expected or could be done better.&lt;/p&gt; &lt;p&gt;KDE has a bug tracking system. Visit &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; or use the \"Report Bug\" button from the about screen to report bugs.&lt;/p&gt; If you have a suggestion for improvement then you are welcome to use the bug tracking system to register your wish. Make sure you use the severity called \"Wishlist\".</string>
<string name="about_kde_join_kde">&lt;h1&gt;Join KDE&lt;/h1&gt; &lt;p&gt;You do not have to be a software developer to be a member of the KDE team. You can join the language teams that translate program interfaces. You can provide graphics, themes, sounds, and improved documentation. You decide!&lt;/p&gt; &lt;p&gt;Visit &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; for information on some projects in which you can participate.&lt;/p&gt; If you need more information or documentation, then a visit to &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; will provide you with what you need.</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Enviar</string>
<string name="compose_send_title">Componer envío</string>
<string name="open_compose_send">Componer texto</string>
<string name="double_tap_to_drag">Doble pulsación para arrastrar</string>
<string name="hold_to_drag">Mantener para arrastrar</string>
<string name="about_kde_about">&lt;h1&gt;Acerca de&lt;/h1&gt; &lt;p&gt;KDE es una comunidad global de ingenieros software, artistas, escritores, traductores y creadores que siguen el desarrollo de &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Software Libre&lt;/a&gt;. KDE produce el entorno de escritorio Plasma, cientos de aplicaciones y todas las librerías en las que se basan.&lt;/p&gt; &lt;p&gt;KDE es una empresa colaborativa: no hay una entidad única que controla sus productos o su dirección. En su lugar, trabajamos de manera conjunta para conseguir la meta común de construir el mejor software libre posible. Todo el mundo es bienvenido a &lt;a href=https://community.kde.org/Get_Involved&gt;unirse y contribuir&lt;/a&gt; a KDE, incluido usted.&lt;/p&gt; Visite &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; para más información sobre la comunidad KDE y el software que creamos.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Reporte errores o deseos&lt;/h1&gt; &lt;p&gt;El software siempre puede ser mejorado y el equipo de KDE está preparado para ello. Sin embargo, usted - el usuario - debe comunicarnos cuando algo no funciona como es esperado o que puede ser mejorado. &lt;/p&gt; &lt;p&gt; KDE tiene un sistema de traqueo de errores. Visite &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; o use el botón «Informar de fallo» en la ventana «Acerca de» para reportar errores.&lt;/p&gt; Si tiene una sugerencia de mejora entonces use el sistema de traqueo de errores para registrar su sugerencia. Asegúrese de que usa la severidad «Lista de deseos».</string>
<string name="about_kde_join_kde">&lt;h1&gt;Unirse a KDE&lt;/h1&gt; &lt;p&gt;No tiene por que ser un desarrollador de software para ser miembro del equipo KDE. Se puede unir a los equipos de traducción que traducen las interfaces de los programas. Puede proporcionar gráficos, temas, sonidos y mejorar la documentación. ¡Tú decides!&lt;/p&gt; &lt;p&gt;Visite &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; para más información sobre los proyectos en los que puede participar.&lt;/p&gt; Si necesita más información o documentación, entonces una visita a &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; le proporcionará la información que necesite.</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Bidali</string>
<string name="compose_send_title">Bidalketa osatu</string>
<string name="open_compose_send">Konposatu testua</string>
<string name="double_tap_to_drag">Tak bikoitza arrastatzeko</string>
<string name="hold_to_drag">Eutsi arrastatzeko</string>
<string name="about_kde_about">"&lt;h1&gt;Honi buruz&lt;/h1&gt; &lt;p&gt;KDE &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Software Askearen&lt;/a&gt; garapenarekin engaiatutako mundu osoko software ingeniari, artista, idazle, itzultzaile eta sortzaile elkarte bat da. KDEk Plasma mahaigain ingurunea, ehunka aplikazio, eta haiei euskarria ematen dieten liburutegi ugariak ekoizten ditu. &lt;/p&gt; &lt;p&gt;KDE ekimen kooperatibo bat da: ez dago bere norabidea eta produktuak kontrolatzen dituen erakunderik. Aldiz, elkarrekin lan egiten dugu guztiok partekatzen dugun helburu bera lortzeko, munduko Software Aske bikainena eraikitzearena alegia. Jende oro ongi etorria da KDErekin &lt;a href=https://community.kde.org/Get_Involved&gt;elkartu eta laguntza ematera&lt;/a&gt;, zu barne. &lt;/p&gt; Bisitatu &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; , KDE elkartearen eta ekoizten dugun softwarearen gaineko informazio zabalagoa eskuratzeko."</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Akatsen edo nahien berri ematea&lt;/h1&gt; &lt;p&gt;Softwarea beti hobetu daiteke, eta KDE taldea horretarako prest dago. Hala ere, zuk - erabiltzailea zaren horrek - zerbait behar bezala ez dabilenean edo hobeto egin daitekeenean esan egin behar diguzu.&lt;/p&gt; &lt;p&gt;KDEk programa-akatsen gaineko jarraipena egiteko sistema bat du. Bisitatu &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; edo erabili Honi buruz pantailako «Akatsa jakinarazi» botoia.&lt;/p&gt; Hobetzeko iradokizunik baduzu, akatsen jarraipen sisteman zure nahia erregistratzera gonbidatzen zaitugu. Larritasun maila bezala \"Wishlist\" (nahien zerrenda) erabil ezazu horretarako.</string>
<string name="about_kde_join_kde">"&lt;h1&gt;Zatoz KDEra&lt;/h1&gt; &lt;p&gt;Ez duzu software garatzailea izan behar KDEren taldeko kide izateko. Programen interfazeak itzultzen dituzten hizkuntzen taldeetara batu zaitezke. Grafikoak, gaiak, soinuak, eta dokumentazio hobetua eskain ditzakezu. Zeuk erabaki!&lt;/p&gt; &lt;p&gt;Bisitatu &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; parte hartu dezakezun proiektuen informazioa eskuratzeko.&lt;/p&gt; Informazio edo dokumentazio gehiago behar baduzu, &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; bisitatuz behar duzuna eskuratuko duzu."</string>

View File

@@ -53,7 +53,7 @@
<string name="open_mousepad">Contrôle distant</string>
<string name="mousepad_info">Faites glisser votre doigt sur l\'écran pour déplacer le pointeur de la souris. Tapotez pour cliquer et utilisez deux / trois doigts pour les clics droit et centre. Utilisez 2 doigts pour faire un défilement. Faites un appui prolongé pour réaliser un glisser-déposer. La fonctionnalité de gyroscope de souris peut être activée à partir des préférences de module externe.</string>
<string name="mousepad_info_no_gestures">Faites glisser un doigt sur l\'écran pour déplacer le pointeur de souris. Tapotez sur l\'écran pour effectuer un clic.</string>
<string name="mousepad_keyboard_input_not_supported">La saisie par le clavier n\'est pas pris en charge par le périphérique appairée.</string>
<string name="mousepad_keyboard_input_not_supported">La saisie par le clavier n\'est pas pris en charge par le périphérique associé.</string>
<string name="mousepad_single_tap_settings_title">Définir une action pour tapotage avec un doigt</string>
<string name="mousepad_double_tap_settings_title">Action pour l\'appui à deux doigts</string>
<string name="mousepad_triple_tap_settings_title">Action pour l\'appui à trois doigts</string>
@@ -99,7 +99,7 @@
<string name="pref_plugin_mousepad_send_keystrokes">Envoyez comme appuis de touches</string>
<string name="mouse_receiver_plugin_description">Recevoir les mouvements de la souri distante</string>
<string name="mouse_receiver_plugin_name">Récepteur de souris</string>
<string name="mouse_receiver_no_permissions">Pour recevoir des entrées tactiles à distance, vous devez accorder des autorisations daccessibilité pour contrôler entièrement votre périphérique</string>
<string name="mouse_receiver_no_permissions">Pour recevoir des entrées tactiles à distance, vous devez accorder des autorisations d\'accessibilité pour contrôler entièrement votre périphérique</string>
<string name="view_status_title">État</string>
<string name="battery_status_format">Batterie : %d %%</string>
<string name="battery_status_low_format">Batterie : %d %% Batterie faible</string>
@@ -186,12 +186,12 @@
<item>1 minute</item>
<item>2 minutes</item>
</string-array>
<string name="mpris_notifications_explanation">Les permissions pour les notifications sont nécessaires pour afficher des supports distants dans le panneau des notifications</string>
<string name="mpris_notifications_explanation">Les permissions pour les notifications sont nécessaires pour afficher des média distants dans le panneau des notifications.</string>
<string name="mpris_notification_settings_title">Afficher la notification de contrôle du lecteur multimédia</string>
<string name="mpris_notification_settings_summary">Vous permet de contrôler vos lecteurs multimédia sans ouvrir KDEConnect.</string>
<string name="share_to">Partager vers…</string>
<string name="unreachable_device">%s (Inaccessible)</string>
<string name="unreachable_device_url_share_text">Les URL partagées vers un appareil inaccessible lui seront transmises une fois quil re-deviendra accessible.\n\n</string>
<string name="unreachable_device_url_share_text">Les URL partagées vers un appareil inaccessible lui seront transmises une fois qu\'il re-deviendra accessible.\n\n</string>
<string name="protocol_version_newer">Le périphérique utilise une version plus récente du protocole</string>
<string name="plugin_settings_with_name">Configuration %s</string>
<string name="invalid_device_name">Nom de périphérique non valable</string>
@@ -389,6 +389,8 @@
<string name="send_compose">Envoyer</string>
<string name="compose_send_title">Préparer l\'envoi</string>
<string name="open_compose_send">Composer du texte</string>
<string name="double_tap_to_drag">Tapotement double pour un glisser</string>
<string name="hold_to_drag">Maintenir pour faire glisser</string>
<string name="about_kde_about">&lt;h1&gt;A propos&lt;/h1&gt; &lt;p&gt;KDE est une communauté mondiale d\'ingénieurs en logiciel, d\'artistes d\'ingénieurs logiciels, d\'artistes, d\'écrivains, de traducteurs et de créateurs s\'engageant pour le développement de &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt; Logiciels libres &lt;/a &gt;. KDE développe l\'environnement de bureau Plasma, des centaines d\'applications, et les nombreuses bibliothèques logicielles les prenant en charge. KDE est une entreprise coopérative : aucune entité centrale ne contrôle sa direction ou ses produits. Au contraire, nous travaillons tous ensemble pour atteindre un objectif commun : construire le meilleur logiciel libre au monde. Tout le monde est est le bienvenu pour &lt;a href=https://community.kde.org/Get_Involved&gt;rejoindre et contribuer&lt;/a&gt; à KDE, y compris vous. &lt;/p&gt; Visitez &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; pour de plus amples informations sur la communauté KDE et les logiciels que nous développons.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Signaler des bogues ou des souhaits&lt;/h1&gt; &lt;p&gt; Les logiciels peuvent toujours être améliorés et l\'équipe KDE est prête à le faire. Cependant, vous - la personne utilisatrice - devez nous dire quand quelque chose ne fonctionne pas comme prévu ou pourrait être mieux fait. KDE dispose d\'un système de suivi des bogues. Visitez &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; ou utilisez le bouton bouton « Signaler un bogue » de la page « A propos » pour signaler les bogues. Si vous avez une suggestion d\'amélioration, vous pouvez aussi utiliser le système de suivi des bogues pour enregistrer votre souhait. Veuillez vous assurer de bien utiliser le niveau de gravité appelée « Liste de souhaits ».</string>
<string name="about_kde_join_kde">&lt;h1&gt;Rejoignez KDE&lt;/h1&gt; &lt;p&gt;Vous n\'avez pas besoin d\'être un développeur de logiciels pour être membre de l\'équipe de KDE. Vous pouvez rejoindre les équipes nationales qui traduisent les interfaces des programmes. Vous pouvez fournir des illustrations, des thèmes, des sons, et de la documentation améliorée. À vous de décider ! &lt;/p&gt; &lt;p&gt;Visitez la page &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; pour obtenir des informations sur certains projets auxquels vous pouvez participer.&lt;/p&gt; Si vous avez besoin de plus d\'informations ou de documentation, veuillez visitez la page &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt;, qui vous fournira ce dont vous avez besoin.</string>
@@ -403,7 +405,8 @@
<string name="maxim_leshchenko_task">Améliorations de l\'interface utilisateur et de cette page d\'à propos</string>
<string name="holger_kaelberer_task">Corrections du module externe de clavier sans fil et de bogues</string>
<string name="saikrishna_arcot_task">Prise en charge de l\'utilisation du clavier dans le module d\'entrée à distance, corrections de bogues et améliorations générales</string>
<string name="everyone_else">"Toutes les autres personnes ayant contribué KDEConnect depuis plusieurs années"</string>
<string name="shellwen_chen_task">Implémentation de la sécurité de SFTP, amélioration de la maintenabilité de ce projet, corrections de bogues et améliorations générales</string>
<string name="everyone_else">Toutes les autres personnes ayant contribué à KDEConnect depuis plusieurs années</string>
<string name="send_clipboard">Envoyer le presse-papier</string>
<string name="tap_to_execute">Tapotez pour lancer</string>
<string name="plugin_stats">Statistiques des modules externes</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Enviar</string>
<string name="compose_send_title">Preparar un envío</string>
<string name="open_compose_send">Escribir texto</string>
<string name="double_tap_to_drag">Toque dúas veces para arrastrar</string>
<string name="hold_to_drag">Manteña para arrastrar</string>
<string name="about_kde_about">"&lt;h1&gt;Sobre&lt;/h1&gt; &lt;p&gt;KDE é unha comunidade internacional de persoas dedicadas á enxeñaría de soporte lóxico, á arte, á documentación, á tradución e á creación, todas elas comprometidas co desenvolvemento de &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;soporte lóxico libre&lt;/a&gt;. KDE produce o contorno de escritorio Plasma, centos de aplicacións, e as moitas bibliotecas de soporte lóxico sobre as que estas están construídas.&lt;/p&gt; &lt;p&gt;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 soporte lóxico libre do mundo. Todas as persoas son benvidas a &lt;a href=https://community.kde.org/Get_Involved&gt;unirse e colaborar&lt;/a&gt; en KDE, incluída vostede.&lt;/p&gt; Visite &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; para máis información sobre a comunidade KDE e o soporte lóxico que produce."</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Informe de fallos ou pida melloras&lt;/h1&gt; &lt;p&gt;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.&lt;/p&gt; &lt;p&gt;KDE ten un sistema de seguimento de fallos. Visite &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; ou use o botón de «Informar dun fallo» da pantalla de información para informar dun fallo.&lt;/p&gt; 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">&lt;h1&gt;Únase a KDE&lt;/h1&gt; &lt;p&gt;Non necesita saber desenvolver software para formar parte do equipo de KDE. Pode unirse aos equipos de idiomas que traducen as interfaces dos programas. Pode fornecer imaxes, temas, sons, e mellorar a documentación. Vostede decide!&lt;/p&gt; &lt;p&gt;Visite &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; para informarse sobre os proxectos nos que pode participar.&lt;/p&gt; Se necesita máis información ou documentación, ten o que necesita en &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt;.</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Invia</string>
<string name="compose_send_title">Invio scorciatoia composita</string>
<string name="open_compose_send">Componi il testo</string>
<string name="double_tap_to_drag">Doppio tocco per trascinare</string>
<string name="hold_to_drag">Tieni premuto per trascinare</string>
<string name="about_kde_about">&lt;h1&gt;Informazioni&lt;/h1&gt; &lt;p&gt;KDE è una comunità mondiale di ingegneri del software, artisti, scrittori, traduttori e creatori che si impegnano a sviluppare &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;software libero&lt;/a&gt;. KDE produce l\'ambiente desktop Plasma, centinaia di applicazioni e le numerose librerie software che le supportano.&lt;/p&gt; &lt;p&gt;KDE è un\'impresa cooperativa: nessuna singola entità ne controlla la direzione o i prodotti. Invece, lavoriamo insieme per raggiungere l\'obiettivo comune di costruire il miglior software libero del mondo. Tutti sono invitati a &lt;a href=https://community.kde.org/Get_Involved&gt;unirsi e contribuire&lt;/a&gt; a KDE, incluso te.&lt;/p&gt; Visita &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; per ulteriori informazioni sulla comunità KDE e sul software che produciamo.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Segnala bug o desideri&lt;/h1&gt; &lt;p&gt;Il software può sempre essere migliorato e il team di KDE è pronto a farlo. Tuttavia, tu - l\'utente - devi dirci quando qualcosa non funziona come previsto o potrebbe essere fatto meglio.&lt;/p&gt; &lt;p&gt;KDE ha un sistema di tracciamento dei bug. Visita &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; oppure utilizza il pulsante «Segnala bug» dalla schermata delle informazioni per segnalare i bug.&lt;/p&gt; Se hai un suggerimento per il miglioramento, puoi utilizzare il sistema di tracciamento dei bug per registrare il tuo desiderio. Assicurati di utilizzare «Wishlist» per il campo Severity.</string>
<string name="about_kde_join_kde">&lt;h1&gt;Unisciti a KDE&lt;/h1&gt; &lt;p&gt;Non devi essere uno sviluppatore di software per essere un membro della squadra di KDE. Puoi unirti ai gruppi linguistici che traducono le interfacce dei programmi. Puoi fornire grafica, temi, suoni e documentazione migliorata. Decidi tu!&lt;/p&gt; &lt;p&gt;Visita &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; per informazioni su alcuni progetti a cui puoi partecipare.&lt;/p&gt; Se hai bisogno di ulteriori informazioni o di documentazione, visita &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; ti fornirà quello che ti serve.</string>

View File

@@ -405,6 +405,8 @@
<string name="send_compose">שליחה</string>
<string name="compose_send_title">שליחה מחיבור</string>
<string name="open_compose_send">טקסט חיבור</string>
<string name="double_tap_to_drag">נקישה כפולה לגרירה</string>
<string name="hold_to_drag">החזקה לגרירה</string>
<string name="about_kde_about">&lt;h1&gt;על אודות&lt;/h1&gt; &lt;p&gt;KDE היא קהילה בינלאומית של מהנדסי תוכנה, כותבים, מתרגמים ויוצרים שמחויבים לפיתוח &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;תוכנה חופשית&lt;/a&gt;. מיזם KDE מפיק את סביבת שולחן העבודה פלזמה, מאות יישומים ומגוון רחב של ספריות תוכנה כדי לתמוך בהם.&lt;/p&gt; &lt;p&gt;KDE הוא תאגיד חברתי: אף ישות יחידה לא שולטת בכיוון או במוצרים. במקום, אנו פעולים יחד כדי להגיע ליעדנו המשותף: לבנות את התוכנה החופשית הטובה בעולם. כולם מוזמנים &lt;a href=https://community.kde.org/Get_Involved&gt;להצטרף ולתרום&lt;/a&gt; ל־KDE, כולל אותך.&lt;/p&gt; כדאי לבקר באתר &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; למידע נוסף על קהילת KDE ועל התוכנה שאנו מפיקים.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;דיווח על תקלות או משאלות&lt;/h1&gt; &lt;p&gt;תוכנות תמיד יכולות להשתפר וצוות KDE ערוך לכך. עם זאת, אנו זקוקים לך - המשתמש או המשתמשת - כדי לספר לנו מתי משהו לא עובד כצפוי או עשוי לעבוד טוב יותר.&lt;/p&gt; &lt;p&gt;ל־KDE יש מערכת מעקב אחר תקלות. יש לבקר ב־&lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; או להשתמש בכפתור „דיווח על תקלה” ממסך על אודות כדי לדווח על תקלות.&lt;/p&gt; אם יש לך הצעה לשיפור, אנו מזמינים אותך להשתמש במערכת מעקב התקלות כדי לתעד את המשאלה שלך, חשוב לזכור לסמן אותה בדרגת חומרה (severity) בשם „Wishlist” (רשימת משאלות).</string>
<string name="about_kde_join_kde">&lt;h1&gt;הצטרפות ל־KDE&lt;/h1&gt; &lt;p&gt;לא צריך להיות מפתחי תוכנה כדי להצטרף לחברות ב־KDE. אפשר להצטרף לצוותים המקומיים שמתרגמים את ממשקי התוכנות. אפשר לספק גרפיקה, ערכות עיצוב, צלילי ושיפור התיעוד. הבחירה היא רק שלך!&lt;/p&gt; &lt;p&gt;יש לבקר ב־&lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; למידע על חלק מהמיזמים בהם ניתן לקחת חלק.&lt;/p&gt; כדי לקבל מידע או תיעוד נוספים, ביקור ב־&lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; יספק לך את מה שדרוש לך.</string>

View File

@@ -18,6 +18,7 @@
<string name="pref_plugin_clipboard_sent">클립보드 보냄</string>
<string name="pref_plugin_mousepad">원격 입력</string>
<string name="pref_plugin_mousepad_desc">내 휴대폰이나 태블릿을 터치패드와 키보드로 사용하기</string>
<string name="pref_plugin_presenter">프레젠테이션 리모콘</string>
<string name="pref_plugin_presenter_desc">내 장치로 프레젠테이션 슬라이드 전환하기</string>
<string name="pref_plugin_remotekeyboard">원격 키 입력 받기</string>
<string name="pref_plugin_remotekeyboard_desc">원격 장치의 키 입력 이벤트 받기</string>
@@ -51,6 +52,7 @@
<string name="remotekeyboard_multiple_connections">원격 키보드 연결이 여러 개 있습니다. 설정할 장치를 선택하십시오</string>
<string name="open_mousepad">원격 입력</string>
<string name="mousepad_info">화면에서 손가락을 움직이면 마우스 커서를 움직입니다. 화면을 누르면 왼쪽 단추를 누르고, 두 손가락과 세 손가락으로 누르면 오른쪽/가운데 단추를 누릅니다. 두 손가락을 사용하여 스크롤할 수 있습니다. 드래그 앤 드롭을 사용하려면 길게 누르십시오. 플러그인 설정에서 자이로 마우스를 활성화할 수 있습니다</string>
<string name="mousepad_info_no_gestures">화면에서 손가락을 움직이면 마우스 커서를 움직이며, 탭하면 클릭합니다.</string>
<string name="mousepad_keyboard_input_not_supported">페어링된 장치에서 키보드 입력을 지원하지 않음</string>
<string name="mousepad_single_tap_settings_title">한 손가락으로 눌렀을 때 동작 설정</string>
<string name="mousepad_double_tap_settings_title">두 손가락으로 눌렀을 때 동작 설정</string>
@@ -119,6 +121,8 @@
<string name="my_device_fingerprint">내 장치 인증서의 SHA256 지문:</string>
<string name="remote_device_fingerprint">원격 장치 인증서의 SHA256 지문:</string>
<string name="pair_requested">연결 요청됨</string>
<string name="pair_succeeded">연결 성공</string>
<string name="pairing_request_from">\'%1s\'에서 연결 요청</string>
<plurals name="incoming_file_title">
<item quantity="other">%2$s에서 보낸 파일 %1$d개 받음</item>
</plurals>
@@ -147,6 +151,7 @@
<string name="received_file_text">\'%1s\'을(를) 열려면 누르십시오</string>
<string name="cannot_create_file">파일 %s을(를) 만들 수 없음</string>
<string name="tap_to_answer">눌러서 응답하기</string>
<string name="left_click">왼쪽 단추 클릭 신호 보내기</string>
<string name="right_click">오른쪽 단추 클릭 신호 보내기</string>
<string name="middle_click">가운데 단추 클릭 신호 보내기</string>
<string name="show_keyboard">키보드 표시</string>
@@ -176,6 +181,9 @@
<string name="mpris_notifications_explanation">알림 서랍에 원격 미디어를 표시하려면 알림 권한이 필요합니다</string>
<string name="mpris_notification_settings_title">미디어 제어 알림 표시</string>
<string name="mpris_notification_settings_summary">KDE Connect를 열지 않고 미디어 재생기 제어</string>
<string name="share_to">다음으로 공유…</string>
<string name="unreachable_device">%s(접근할 수 없음)</string>
<string name="unreachable_device_url_share_text">접근할 수 없는 장치와 공유한 URL은 장치에 다시 접근할 수 있게 될 때 전달됩니다.\n\n</string>
<string name="protocol_version_newer">이 장치의 프로토콜 버전이 더 새롭습니다</string>
<string name="plugin_settings_with_name">%s 설정</string>
<string name="invalid_device_name">잘못된 장치 이름</string>
@@ -387,6 +395,7 @@
<string name="maxim_leshchenko_task">UI 개선과 정보 페이지</string>
<string name="holger_kaelberer_task">원격 키보드 플러그인과 버그 수정</string>
<string name="saikrishna_arcot_task">원격 입력 플러그인에서 키보드 사용 지원, 버그 수정과 개선</string>
<string name="shellwen_chen_task">SFTP 보안 개선, 프로젝트 관리 편의성 개선, 버그 수정과 개선</string>
<string name="everyone_else">그 외 오랫동안 KDE Connect에 기여한 사람들</string>
<string name="send_clipboard">클립보드 보내기</string>
<string name="tap_to_execute">실행하려면 누르십시오</string>
@@ -395,4 +404,8 @@
<string name="receive_notifications_permission_explanation">다른 장치에서 알림을 받으려면 알림을 허용해야 합니다</string>
<string name="findmyphone_notifications_explanation">앱이 백그라운드에서 실행 중일 때 장치를 울리게 하려면 알림 권한이 필요합니다</string>
<string name="no_notifications">알림이 비활성화되어 있습니다. 들어오는 페어링 알림을 받을 수 없습니다</string>
<string name="mpris_keepwatching">계속 재생</string>
<string name="mpris_keepwatching_settings_title">계속 재생</string>
<string name="mpris_keepwatching_settings_summary">미디어를 닫은 후 이 장치에서 계속 재생할 수 있는 조용한 알림 표시</string>
<string name="notification_channel_keepwatching">계속 재생</string>
</resources>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Verzenden</string>
<string name="compose_send_title">Opstellen van verzending</string>
<string name="open_compose_send">Tekst opstellen</string>
<string name="double_tap_to_drag">Dubbel tikken om te slepen</string>
<string name="hold_to_drag">Ingedrukt houden om te slepen</string>
<string name="about_kde_about">&lt;h1&gt;Info over&lt;/h1&gt; &lt;p&gt;KDE is een wereldwijde gemeenschap van software ingenieurs, artiesten, schrijvers, vertalers en makers die toegewijd zijn aan &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Vrije software&lt;/a&gt; ontwikkeling. KDE produceert de Plasma bureaubladomgeving, honderden toepassingen en de vele software bibliotheken die deze ondersteunen.&lt;/p&gt; &lt;p&gt;KDE is een coöperatieve onderneming: geen enkele entiteit controleert zijn richting of producten. In plaats daarvan werken we samen om het gemeenschappelijke doel te bereiken van het bouwen van de \'s werelds mooiste Vrije software. Iedereen is welkom om &lt;a href=https://community.kde.org/Get_Involved&gt;mee te doen en bij te dragen&lt;/a&gt; aan KDE, inclusief u.&lt;/p&gt; Bezoek &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; voor meer informatie over de KDE gemeenschap en de software die we produceren.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Bugs of wensen rapporteren&lt;/h1&gt; &lt;p&gt;Software kan altijd verbeterd worden en het KDE team is gereed om dat te doen. Echter, u - de gebruiker - moet ons vertellen wanneer iets niet werkt zoals verwacht of beter gedaan kan worden.&lt;/p&gt; &lt;p&gt;KDE heeft een bugvolgsysteem. Bezoek &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; of gebruik de knop \"Bug rapporteren\" uit het Info over scherm om bugs te rapporteren.&lt;/p&gt; Als u een suggestie voor verbetering dan bent u welkom om het bugvolgsysteem te gebruiken om uw wens te registreren. Ga na dat u de ernst genaamd \"Wishlist\" gebruikt.</string>
<string name="about_kde_join_kde">&lt;h1&gt;Mee doen met KDE&lt;/h1&gt; &lt;p&gt;U hoeft geen software ontwerper te zijn om een lid van het KDE-team te worden. U kunt meedoen met een vertaalteam dat interfaces van programma\'s vertaalt. U kunt illustraties, thema\'s, geluiden en verbeterde documentatie leveren. U beslist!&lt;/p&gt; &lt;p&gt;Bezoek &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; voor informatie over projecten waarin u kunt participeren.&lt;/p&gt; Als u meer informatie of documentatie nodig hebt, dan kan een bezoek aan &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; u alles wat u nodig hebt leveren.</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Send</string>
<string name="compose_send_title">Send tekst</string>
<string name="open_compose_send">Skriv tekst</string>
<string name="double_tap_to_drag">Dobbelttrykk for å dra</string>
<string name="hold_to_drag">Hald for å dra</string>
<string name="about_kde_about">&lt;h1&gt;Om&lt;/h1&gt; &lt;p&gt;KDE er eit verdsfemnande fellesskap av eldsjeler som programmerer, teiknar, komponerer, dokumenterer, set om eller hjelper til på andre måtar med utvikling av &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;fri programvare&lt;/a&gt;. Me har laga brukarflata Plasma, hundrevis av program og dei mange programbiblioteka desse byggjer på.&lt;/p&gt; &lt;p&gt;KDE er eit fellesskap der inga einskild gruppe, firma eller organisasjon har eigarskap til produkta eller styrer retninga den vidare utviklinga skal gå i. Derimot arbeider me saman om å oppnå vårt felles mål om å laga fri programvare i verdsklasse. Alle er &lt;a href=https://community.kde.org/Get_Involved&gt;velkomne til å bidra&lt;/a&gt;  du òg.&lt;/p&gt;Du finn meir informasjon om KDE og programma me utviklar på &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt;.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Meld frå om feil eller ønskje&lt;/h1&gt; &lt;p&gt;Ein kan alltid forbetra programvare, og KDE-gruppa arbeider heile tida for det. Men du, som brukar, må melda frå til oss når noko ikkje verkar slik du forventar, eller når noko kunne vore gjort betre.&lt;/p&gt; &lt;p&gt;KDE har eit feilsporingssystem. Gå til &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; eller vel «Meld frå om feil» på «Om»-sida for å melda frå om feil.&lt;/p&gt; Om du har framlegg til forbetringar, kan du gjerne registrera òg desse i feilsporingssystemet. Sjå då til at du har markert feilrapporten med «Wishlist».</string>
<string name="about_kde_join_kde">&lt;h1&gt;Vert med i KDE&lt;/h1&gt; &lt;p&gt;Du treng ikkje vera programutviklar for å hjelpa til med KDE. Du kan arbeida med omsetjingar, laga grafikk, tema, lydar eller betre hjelpetekstar. Her er noko for alle!&lt;/p&gt; &lt;p&gt;&lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; finn du informasjon om nokre prosjekt du kan delta i.&lt;/p&gt; Om du vil ha meir informasjon eller dokumentasjon, finn du det du treng på &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt;.</string>

View File

@@ -405,6 +405,8 @@
<string name="send_compose">Wyślij</string>
<string name="compose_send_title">Napisz do wysłania</string>
<string name="open_compose_send">Napisz tekst</string>
<string name="double_tap_to_drag">Stuknij dwukrotnie, aby przeciągnąć</string>
<string name="hold_to_drag">Przytrzymaj, aby przeciągnąć</string>
<string name="about_kde_about">"&lt;h1&gt;O programie&lt;/h1&gt; &lt;p&gt;KDE to światowa społeczność inżynierów oprogramowania, artystów, pisarzy, tłumaczy i twórców, którzy są częścią rozwoju &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Wolnego Oprogramowania&lt;/a&gt;. KDE tworzy środowisko pulpitu Plazmy, setki aplikacji i wiele bibliotek programistycznych, aby je wspierać.&lt;/p&gt; &lt;p&gt;KDE jest przedsięwzięciem istniejącym ze współpracy; jego ruchami, czy produktami, nie steruje żaden pojedynczy byt. Pracujemy razem, aby osiągnąć wspólny cel, czyli budowę najlepszego Wolnego Oprogramowania na świecie. Każdy jest mile wiedziany, żeby &lt;a href=https://community.kde.org/Get_Involved&gt;dołączył i zaczął współtworzyć&lt;/a&gt; KDE, włączając w to ciebie.&lt;/p&gt; Odwiedź &lt; href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; po więcej szczegółów nt. społeczności KDE i oprogramowania, które tworzymy."</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Zgłaszaj błędy lub życzenia&lt;/h1&gt; &lt;p&gt;Oprogramowanie zawsze można ulepszyć, a zespół KDE jest gotowy, aby to robić. Jednakże ty - użytkownik - musisz nam powiedzieć o tym, co nie działa jak powinno lub co można zrobić lepiej.&lt;/p&gt; &lt;p&gt;KDE ma system obsługi błędów. Odwiedź &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; lub użyj przycisku \"Zgłoś błąd\" z ekranu o programie do zgłaszania błędów.&lt;/p&gt; Jeśli masz sugestie nt. usprawnień, to także możesz ją zarejestrować w naszym systemie obsługi błędów. Upewnij się, że użyjesz ważności o nazwie \"Lista życzeń\".</string>
<string name="about_kde_join_kde">&lt;h1&gt;Dołącz do KDE&lt;/h1&gt; &lt;p&gt;Nie musisz być programistą, aby być członkiem zespołu KDE. Możesz dołączyć do zespołów językowych, które tłumaczą oprogramowanie. Możesz dostarczać grafikę, zestawy wyglądu, dźwięki i ulepszać dokumentację. To ty decydujesz!&lt;/p&gt; &lt;p&gt;odwiedź &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; po szczegóły nt. projektów, w których możesz wziąć udział .&lt;/p&gt; Jeśli potrzebujesz więcej szczegółów lub dokumentacji, to odwiedziny na &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; dadzą ci to, czego chcesz.</string>

View File

@@ -18,6 +18,7 @@
<string name="pref_plugin_clipboard_sent">Área de transferência enviada</string>
<string name="pref_plugin_mousepad">Introdução de dados remota</string>
<string name="pref_plugin_mousepad_desc">Use seu celular ou tablet como mouse e teclado</string>
<string name="pref_plugin_presenter">Controle remoto para apresentações</string>
<string name="pref_plugin_presenter_desc">Use o seu dispositivo para mudar os slides de uma apresentação</string>
<string name="pref_plugin_remotekeyboard">Receber pressionamento de teclas remoto</string>
<string name="pref_plugin_remotekeyboard_desc">Recebe os eventos de pressionamento de teclas dos dispositivos remotos</string>
@@ -51,6 +52,7 @@
<string name="remotekeyboard_multiple_connections">Existe mais que uma conexão a teclados remotos. Selecione o dispositivo a configurar</string>
<string name="open_mousepad">Introdução de dados remota</string>
<string name="mousepad_info">Mova um dedo pela tela para mover o ponteiro do mouse. Dê um toque para clicar e use dois/três dedos para os botões da direita e do meio. Use dois dedos para rolar a tela. Use uma pressão longa para arrastar e soltar. A funcionalidade do mouse giroscópio pode ser habilitada nas preferências do plugin.</string>
<string name="mousepad_info_no_gestures">Mova o dedo na tela para mover o cursor do mouse, toque para um clique.</string>
<string name="mousepad_keyboard_input_not_supported">Entrada de teclado não suportada pelo dispositivo emparelhado</string>
<string name="mousepad_single_tap_settings_title">Definir ação do toque com um dedo</string>
<string name="mousepad_double_tap_settings_title">Definir ação do toque com dois dedos</string>
@@ -59,6 +61,7 @@
<string name="mousepad_mouse_buttons_title">Mostrar botões do mouse</string>
<string name="mousepad_acceleration_profile_settings_title">Definir aceleração do ponteiro</string>
<string name="mousepad_scroll_direction_title">Direção de rolagem inversa</string>
<string name="mousepad_scroll_sensitivity_title">Sensibilidade da rolagem</string>
<string name="gyro_mouse_enabled_title">Ativar mouse giroscópio</string>
<string name="gyro_mouse_sensitivity_title">Sensibilidade do giroscópio</string>
<string-array name="mousepad_tap_entries">
@@ -118,6 +121,8 @@
<string name="my_device_fingerprint">A impressão digital SHA256 do certificado do seu dispositivo é:</string>
<string name="remote_device_fingerprint">A impressão digital SHA256 do certificado do dispositivo remoto é:</string>
<string name="pair_requested">Solicitação de emparelhamento</string>
<string name="pair_succeeded">Emparelhado com sucesso</string>
<string name="pairing_request_from">Emparelhamento solicitado por %1s</string>
<plurals name="incoming_file_title">
<item quantity="one">Recebendo %1$d arquivo de %2$s</item>
<item quantity="other">Recebendo %1$d arquivos de %2$s</item>
@@ -154,6 +159,7 @@
<string name="received_file_text">Toque para abrir o \'%1s\'</string>
<string name="cannot_create_file">Não foi possível criar o arquivo %s</string>
<string name="tap_to_answer">Toque para responder</string>
<string name="left_click">Enviar um clique de botão esquerdo</string>
<string name="right_click">Enviar um Botão Direito</string>
<string name="middle_click">Enviar um Botão do Meio</string>
<string name="show_keyboard">Mostrar teclado</string>
@@ -180,8 +186,12 @@
<item>1 minuto</item>
<item>2 minutos</item>
</string-array>
<string name="mpris_notifications_explanation">A permissão de notificações é necessária para exibir a mídia remotas na gaveta de notificações</string>
<string name="mpris_notification_settings_title">Mostrar a notificação do controle multimídia</string>
<string name="mpris_notification_settings_summary">Permite controlar os seus reprodutores de mídias sem abrir o KDE Connect</string>
<string name="share_to">Compartilhar com...</string>
<string name="unreachable_device">%s (Inalcançável)</string>
<string name="unreachable_device_url_share_text">URLs compartilhadas com um dispositivo inalcançável serão enviadas para ele assim que ele voltar a ficar disponível.\n\n</string>
<string name="protocol_version_newer">Este dispositivo usa uma versão mais recente do protocolo</string>
<string name="plugin_settings_with_name">Configurações de %s</string>
<string name="invalid_device_name">Nome do dispositivo inválido</string>
@@ -248,10 +258,12 @@
<string name="optional_permission_explanation">Você precisa conceder permissões extras para ativar todas as funções</string>
<string name="plugins_need_optional_permission">Alguns plugins possuem recursos desativados devido à falta de permissões (toque para obter mais informações):</string>
<string name="share_optional_permission_explanation">Para receber arquivos você precisa permitir o acesso ao armazenamento</string>
<string name="share_notifications_explanation">Para acompanhar o progresso ao enviar e receber arquivos, você precisa permitir as notificações</string>
<string name="telepathy_permission_explanation">Para ler e gravar SMS a partir do seu ambiente de trabalho é necessário conceder permissão para SMS</string>
<string name="telephony_permission_explanation">Para ver as chamadas telefônicas no seu ambiente de trabalho é preciso dar permissões para registro de chamadas telefônicas e do estado do celular</string>
<string name="telephony_optional_permission_explanation">Para ver o nome de um contato em vez do seu número de telefone é necessário conceder acesso aos contatos do celular</string>
<string name="contacts_permission_explanation">Para compartilhar o seu livro de endereços com o ambiente de trabalho é necessário conceder permissão para os contatos</string>
<string name="contacts_per_device_confirmation">Os contatos do seu telefone serão copiados para este dispositivo, dessa forma eles poderão ser utilizados pelo aplicativo de SMS do KDE Connect e por outros.</string>
<string name="select_ringtone">Selecionar um toque de chamada</string>
<string name="telephony_pref_blocked_title">Números bloqueados</string>
<string name="telephony_pref_blocked_dialog_desc">Não mostrar as chamadas e SMS destes números. Indique um número por linha.</string>
@@ -269,6 +281,9 @@
<string name="notification_channel_default">Outras notificações</string>
<string name="notification_channel_persistent">Indicador persistente</string>
<string name="notification_channel_media_control">Controle multimídia</string>
<string name="notification_channel_filetransfer">Chegada de arquivo via transferência</string>
<string name="notification_channel_filetransfer_upload">Saída de arquivo via transferência</string>
<string name="notification_channel_filetransfer_error">Erro na transferência de arquivo</string>
<string name="notification_channel_high_priority">Prioridade alta</string>
<string name="mpris_stop">Parar o reprodutor atual</string>
<string name="copy_url_to_clipboard">Copiar URL para a área de transferência</string>
@@ -277,6 +292,10 @@
<string name="runcommand_notpaired">Dispositivo não emparelhado</string>
<string name="runcommand_nosuchdevice">Este dispositivo não existe</string>
<string name="runcommand_noruncommandplugin">Este dispositivo não tem o plugin \'Executar comando\' ativo</string>
<string name="runcommand_category_device_controls_title">Controles do dispositivo</string>
<string name="runcommand_device_controls_summary">Caso o seu dispositivo suporte \"controles de dispositivo\", os comandos que você configurou aparecerão aqui.</string>
<string name="set_runcommand_name_as_title">set_runcommand_name_as_title</string>
<string name="runcommand_name_as_title_title">Exibir o nome como título</string>
<string name="pref_plugin_findremotedevice">Procurar dispositivo remoto</string>
<string name="pref_plugin_findremotedevice_desc">Fazer tocar o seu dispositivo remoto</string>
<string name="ring">Toque</string>
@@ -359,7 +378,7 @@
<string name="email_contributor">E-mail do colaborador\n%s</string>
<string name="visit_contributors_homepage">Visite o site do colaborador\n%s</string>
<string name="version">Versão %s</string>
<string name="about_kde">Sobre o KDE</string>
<string name="about_kde">Sobre a KDE</string>
<string name="kde_be_free">KDE - Seja livre!</string>
<string name="kde">KDE</string>
<string name="konqi">Konqi</string>
@@ -370,9 +389,12 @@
<string name="send_compose">Enviar</string>
<string name="compose_send_title">Enviar composto</string>
<string name="open_compose_send">Compor texto</string>
<string name="about_kde_about">"&lt;h1&gt;About&lt;/h1&gt; &lt;p&gt;O KDE é uma comunidade mundial de engenheiros de software, artistas, escritores, tradutores e criadores comprometidos com o desenvolvimento de &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Software Livre.&lt;/a&gt; O KDE produz o ambiente de desktop Plasma, centenas de aplicativos e muitas bibliotecas de software que os suportam.&lt;/p&gt; &lt;p&gt;KDE é uma empresa cooperativa: nenhuma entidade controla sua direção ou produtos. Em vez disso, trabalhamos juntos para alcançar o objetivo comum de construir o melhor software livre do mundo. Todos são bem-vindos para &lt;a href=https://community.kde.org/Get_Involved&gt;participar e contribuir com o KDE&lt;/a&gt; , inclusive você.&lt;/p&gt; Visite &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; para obter mais informações sobre a comunidade KDE e o software que produzimos."</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Reportar bugs ou sugestões&lt;/h1&gt; &lt;p&gt; Software sempre pode ser melhorado e a equipe do KDE está pronta para isso. No entanto, você - o usuário - deve nos informar quando algo não funcionar conforme o esperado ou puder ser feito melhor.&lt;/p&gt; &lt;p&gt;KDE possui um sistema de rastreamento de bugs. Visite &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; ou use o botão \"Relatar erro\" na tela \"sobre\" para relatar bugs.&lt;/p&gt; Se você tiver uma sugestão de melhoria, poderá usar o sistema de rastreamento de bugs para registrar sua solicitação. Certifique-se de usar a opção \"wishlist\" no campo \"severity\".</string>
<string name="about_kde_support_kde">&lt;h1&gt;Apoie o KDE&lt;/h1&gt; &lt;p&gt;O software do KDE está e sempre estará disponível gratuitamente, mas criá-lo não é gratuito.&lt;/p&gt; &lt;p&gt;Para apoiar o desenvolvimento, a comunidade KDE formou o KDE e.V., uma organização sem fins lucrativos fundada legalmente na Alemanha. KDE e.V. representa a comunidade KDE em questões legais e financeiras. Veja &lt;a href=https://ev.kde.org/&gt;https://ev.kde.org/&lt;/a&gt; para obter informações sobre o KDE e.V.&lt;/p&gt; &lt;p&gt;O KDE se beneficia de muitos tipos de contribuições, inclusive financeiras. Usamos os fundos para reembolsar os membros e outros pelas despesas em que incorrem ao contribuir. Outros fundos são usados para apoio jurídico e organização de conferências e reuniões.&lt;/p&gt; &lt;p&gt;Gostaríamos de incentivá-lo a apoiar nossos esforços com uma doação financeira, usando uma das formas descritas em &lt;a href=https://www.kde.org/community/donations/&gt;https://www.kde.org/community/donations/&lt;/a&gt;.&lt;/p&gt; Muito obrigado antecipadamente por seu apoio.</string>
<string name="double_tap_to_drag">Toque duplo para arrastar</string>
<string name="hold_to_drag">Segurar para arrastar</string>
<string name="about_kde_about">&lt;h1&gt;About&lt;/h1&gt; &lt;p&gt;A KDE é uma comunidade mundial de engenheiros de software, artistas, escritores, tradutores e criadores comprometidos com o desenvolvimento de &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Software Livre.&lt;/a&gt; A KDE produz o ambiente de desktop Plasma, centenas de aplicativos e muitas bibliotecas de software que os suportam.&lt;/p&gt; &lt;p&gt;A KDE é uma empresa cooperativa: nenhuma entidade controla sua direção ou produtos. Em vez disso, trabalhamos juntos para alcançar o objetivo comum de construir o melhor software livre do mundo. Todos são bem-vindos para &lt;a href=https://community.kde.org/Get_Involved&gt;participar e contribuir com a KDE&lt;/a&gt; , inclusive você.&lt;/p&gt; Visite &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; para obter mais informações sobre a comunidade KDE e o software que produzimos.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Reportar bugs ou sugestões&lt;/h1&gt; &lt;p&gt; Software sempre pode ser melhorado e a equipe do KDE está pronta para isso. No entanto, você - o usuário - deve nos informar quando algo não funcionar conforme o esperado ou puder ser feito melhor.&lt;/p&gt; &lt;p&gt;A KDE possui um sistema de rastreamento de bugs. Visite &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; ou use o botão \"Relatar erro\" na tela \"sobre\" para relatar bugs.&lt;/p&gt; Se você tiver uma sugestão de melhoria, poderá usar o sistema de rastreamento de bugs para registrar sua solicitação. Certifique-se de usar a opção \"wishlist\" no campo \"severity\".</string>
<string name="about_kde_join_kde">&lt;h1&gt;Junte-se à KDE&lt;/h1&gt; &lt;p&gt; Você não precisa ser um desenvolvedor de software para ser membro da equipe KDE. Você pode se juntar às equipes de idioma que traduzem interfaces de programas. Você pode fornecer imagens, temas, sons e documentação aprimorada. Você decide!&lt;/p&gt; &lt;p&gt;Visite &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; para obter informações sobre alguns projetos dos quais você pode participar.&lt;/p&gt; Caso você precise de mais informações ou documentação, visite &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; talvez você encontre o que procura.</string>
<string name="about_kde_support_kde">&lt;h1&gt;Apoie a KDE&lt;/h1&gt; &lt;p&gt;O software da KDE está e sempre estará disponível gratuitamente, mas criá-lo não é gratuito.&lt;/p&gt; &lt;p&gt;Para apoiar o desenvolvimento, a comunidade KDE formou a KDE e.V., uma organização sem fins lucrativos fundada legalmente na Alemanha. KDE e.V. representa a comunidade KDE em questões legais e financeiras. Veja &lt;a href=https://ev.kde.org/&gt;https://ev.kde.org/&lt;/a&gt; para obter informações sobre o KDE e.V.&lt;/p&gt; &lt;p&gt;A KDE se beneficia de muitos tipos de contribuições, inclusive financeiras. Usamos os fundos para reembolsar os membros e outros pelas despesas em que incorrem ao contribuir. Outros fundos são usados para apoio jurídico e organização de conferências e reuniões.&lt;/p&gt; &lt;p&gt;Gostaríamos de incentivar você a apoiar nossos esforços com uma doação financeira, usando uma das formas descritas em &lt;a href=https://www.kde.org/community/donations/&gt;https://www.kde.org/community/donations/&lt;/a&gt;.&lt;/p&gt; Agradecemos, antecipadamente, o seu apoio.</string>
<string name="maintainer_and_developer">Mantenedor e desenvolvedor</string>
<string name="developer">Desenvolvedor</string>
<string name="apple_support">Suporte ao macOS. Trabalhando no suporte ao iOS</string>
@@ -383,8 +405,17 @@
<string name="maxim_leshchenko_task">Melhorias na interface e nesta página Sobre</string>
<string name="holger_kaelberer_task">Plugin de teclado remoto e correções de erros</string>
<string name="saikrishna_arcot_task">Suporte para usar o teclado no plugin de entrada remoto, correções de erros e melhorias em geral</string>
<string name="everyone_else">Todos os outros que contribuíram para o KDE Connect ao longo dos anos</string>
<string name="shellwen_chen_task">Melhorar a segurança do SFTP, melhorar a capacidade de manutenção deste projeto, correções de erros e melhorias em geral</string>
<string name="everyone_else">Todo mundo que contribuiu para o KDE Connect ao longo dos anos</string>
<string name="send_clipboard">Enviar para área de transferência</string>
<string name="tap_to_execute">Toque para executar</string>
<string name="plugin_stats">Estatísticas do plugin</string>
<string name="enable_udp_broadcast">Habilitar a descoberta de dispositivo via UDP</string>
<string name="receive_notifications_permission_explanation">Notificações devem ser permitidas para receber notificações dos outros dispositivos</string>
<string name="findmyphone_notifications_explanation">A permissão de notificação é necessária para que o telefone possa tocar quando o app estiver em segudo plano</string>
<string name="no_notifications">As notificações estão desabilitadas, você não receberá notificações de solicitação de pareamento.</string>
<string name="mpris_keepwatching">Continuar a execução</string>
<string name="mpris_keepwatching_settings_title">Continuar a execução</string>
<string name="mpris_keepwatching_settings_summary">Exibe uma notificação silenciosa para continuar a execução neste dispositivo após o fechamento da mídia.</string>
<string name="notification_channel_keepwatching">Continuar a execução</string>
</resources>

View File

@@ -405,6 +405,8 @@
<string name="send_compose">Pošlji</string>
<string name="compose_send_title">Sestavite besedilo</string>
<string name="open_compose_send">Sestavite besedilo</string>
<string name="double_tap_to_drag">Dvakrat tapnite za povlek</string>
<string name="hold_to_drag">Držite za povlek</string>
<string name="about_kde_about">&lt;h1&gt;O programu&lt;/h1&gt; &lt;p&gt;KDE je svetovna skupnost programskih inženirjev, umetnikov, piscev, prevajalcev in ustvarjalcev, ki so podporniki razvoja &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Prostega programja&lt;/a&gt;. KDE izdeluje Namizno okolje Plasma, stotine aplikacij in veliko programskih knjižnic, ki jih podpirajo. &lt;/p&gt; &lt;p&gt;KDE je zadružno podjetje: noben deležnik posamično ne obvladuje smeri razvoja ali izdelka. Namesto tega delamo skupaj, da bi dosegli skupni cilj razvoja, da bi prišli do najboljšega prostega programja na svetu. Vsakdo je dobrodošel v skupnost &lt;a href=https://community.kde.org/Get_Involved&gt;da se pridruži in prispeva &lt;/a&gt; v KDE vključno z vami. &lt;/p&gt; Obiščite &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; za več informacij o skupnosti KDE community in programju, ki ga razvijamo.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Poročajte o napakah in željah&lt;/h1&gt; &lt;p&gt;Programje je vedno mogoče izboljšati in skupina KDE je vedno pripravljena na izboljšave. Vendar, vi - kot uporabnica ali uporabnik - nam morate povedati, kadar nekaj ne deluje kot pričakujete ali bi bilo lahko izdelano bolje. &lt;/p&gt; &lt;p&gt;KDE ima sistem sledenja napak. Obiščite &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; ali uporabite gumb \"Poročaj o napaki\" na zaslonu O programu za poročanje napak.&lt;/p&gt; Če imate sugestijo za izboljšanje ste povabljeni, da uporabite sistem za sledenje napakam in zabeležite vašo željo. Zagotovite, da boste resnost napake označili kot \"Seznam želja - Wishlist\".</string>
<string name="about_kde_join_kde">&lt;h1&gt;Pridružite se KDE&lt;/h1&gt; &lt;p&gt;Ni treba, da ste razvijalci programov, da bi bili člani skupnosti KDE. Lahko se pridružite jezikovnim skupinam za prevajanje uporabniških vmesnikov. Lahko prispevate risbe, teme, zvoke in izboljšano dokumentacijo. Sami se odločite!&lt;/p&gt; &lt;p&gt;Obiščite &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; za informacije o projektih, katerim lahko prispevate.&lt;/p&gt; Če potrebujete več informacij ali dokumentacije, potem obiščite &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt;, kjer boste dobili potrebna navodila.</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Skicka</string>
<string name="compose_send_title">Skriv och skicka</string>
<string name="open_compose_send">Skriv text</string>
<string name="double_tap_to_drag">Dubbeltryck för att dra</string>
<string name="hold_to_drag">Håll för att dra</string>
<string name="about_kde_about">&lt;h1&gt;Om&lt;/h1&gt; &lt;p&gt;KDE är ett världsomspännande nätverk av programvaruingenjörer, grafiker, författare, översättare och kreatörer som är engagerade i utveckling av &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;fri programvara&lt;/a&gt;. KDE producerar skrivbordsmiljön Plasma, hundratals program, och de talrika programvarubibliotek som stöder dem.&lt;/p&gt; &lt;p&gt;KDE är ett kooperativ, där ingen enskild person styr inriktningen eller produkterna. Istället arbetar vi tillsammans för att uppnå det gemensamma målet att skapa värdens finaste fria programvara. Alla är välkomna att &lt;a href=https://community.kde.org/Get_Involved&gt;gå med och bidra&lt;/a&gt; till KDE, inklusive du själv.&lt;/p&gt; Besök &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; för ytterligare information om KDE-gemenskapen och programvaran vi skapar.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Rapportera fel eller önskemål&lt;/h1&gt; &lt;p&gt;Programvara kan alltid förbättras, och KDE-gruppen är beredd att göra det. Men du - användaren - måste berätta för oss om något inte fungerar som förväntat eller kunde ha gjorts bättre.&lt;/p&gt; &lt;p&gt;KDE har ett felrapporteringssystem. Besök &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; eller använd dialogrutan \"Rapportera fel...\" från om-skärmen för att rapportera fel.&lt;/p&gt; Om du har ett förslag på en förbättring kan du använda felrapporteringssystemet för att registrera din önskan. Se då till att du använder allvarlighetsgraden \"Önskan\".</string>
<string name="about_kde_join_kde">&lt;h1&gt;Gå med i KDE&lt;/h1&gt; &lt;p&gt;Du behöver inte vara en programutvecklare för att bli medlem i KDE-gruppen. Du kan gå med i de nationella grupperna som översätter programmens gränssnitt. Du kan bidra med grafik, teman, ljud och förbättrad dokumentation. Det är upp till dig!&lt;/p&gt; &lt;p&gt;Besök &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; för information om några projekt som du kan delta i.&lt;/p&gt;Om du behöver mer information eller dokumentation kommer ett besök på &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; att ge dig det du behöver.</string>

View File

@@ -389,6 +389,8 @@
<string name="send_compose">Gönder</string>
<string name="compose_send_title">Gönderi oluştur</string>
<string name="open_compose_send">Metin oluştur</string>
<string name="double_tap_to_drag">Sürüklemek için çift dokunun</string>
<string name="hold_to_drag">Sürüklemek için tutun</string>
<string name="about_kde_about">&lt;h1&gt;Hakkında&lt;/h1&gt; &lt;p&gt;KDE, &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;Özgür Yazılım&lt;/a&gt; hareketine destek veren yazılım mühendislerinin, sanatçıların, yazarların, çevirmenlerin ve yaratıcıların bir araya geldiği dünya çapında bir topluluktur KDE, Plasma masaüstü ortamını, yüzlerce uygulamayı ve onları destekleyen sayısız yazılım kitaplığını üretir.&lt;/p&gt; &lt;p&gt;KDE, işbirliğine dayalı bir kuruluştur: yönünü veya ürünlerini tek başına denetleyen bir kuruluş yoktur. Bunun yerine, dünyanın en iyi Özgür Yazılımını oluşturma ortak hedefine ulaşmak için birlikte çalışıyoruz. Siz de dahil olmak üzere herkes &lt;a href=https://community.kde.org/Get_Involved&gt;katılabilir&lt;/a&gt; ve katkıda bulunabilir.&lt;/p&gt; KDE topluluğu ve ürettiğimiz yazılımlar hakkında daha fazla bilgi için &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; adresini ziyaret edin.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Hataları veya İsteklerinizi Bildirin&lt;/h1&gt; &lt;p&gt;Yazılımlar her zaman iyileştirilebilir ve KDE takımın bunu yapmaya hazırdır. Ancak siz de bir şey beklendiği gibi gitmezse veya hata verirse bize bildirin.&lt;/p&gt; &lt;p&gt;KDEnin bir hata takip sistemi vardır. &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; adresini ziyaret edin veya hakkında ekranının Hata Bildir düğmesini kullanarak hataları bildirin.&lt;/p&gt; Bir iyileştirme için öneriniz varsa bunu bildirmek için hata takip sistemini kullanabilirsiniz; yalnızca “Wishlist” önceliğini kullandığınızdan emin olun.</string>
<string name="about_kde_join_kde">"&lt;h1&gt;KDEye Katılın&lt;/h1&gt; &lt;p&gt;KDE takımının bir üyesi olmak için yazılım geliştirici olmanıza gerek yoktur. Program arayüzlerini çeviren dil takımlarına katılabilirsiniz. Grafikler, temalar, sesler ve iyileştirilmiş belgelendirme sağlayabilirsiniz. Karar sizin!&lt;/p&gt; &lt;p&gt;Katılabileceğiniz bazı projeler hakkında bilgi almak için &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; sayfasını ziyaret edin.&lt;/p&gt; Daha fazla bilgiye veya belgeye gereksiniminiz varsa &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt; sayfasında aradığınızı bulabilirsiniz."</string>

View File

@@ -405,6 +405,8 @@
<string name="send_compose">Надіслати</string>
<string name="compose_send_title">Надсилання редагованого</string>
<string name="open_compose_send">Редагувати текст</string>
<string name="double_tap_to_drag">Подвійне торкання для перетягування</string>
<string name="hold_to_drag">Утримування для перетягування</string>
<string name="about_kde_about">&lt;h1&gt;Інформація&lt;/h1&gt; &lt;p&gt;KDE — це всесвітня спільнота програмістів, художників, авторів текстів, перекладачів та фахівців з полегшення користування програмами, які роблять свій внесок до розвитку &lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;вільного програмного забезпечення&lt;/a&gt;. KDE створено стільничне середовище Плазма, сотні вільних програм і багато бібліотек, які є їхньою основою.&lt;/p&gt; &lt;p&gt;Розробка KDE є спільною працею, у якій жоден з учасників не має переважного контролю над зусиллями або результатами роботи інших розробників KDE. Ми працюємо разом заради спільної мети — створення найкращого вільного програмного забезпечення. Кожен може &lt;a href=https://community.kde.org/Get_Involved&gt;долучитися і зробити свій внесок&lt;/a&gt;, зокрема це можете зробити ви.&lt;/p&gt; Відвідайте сайт &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt;, щоб дізнатися більше про спільноту KDE та створене нею програмне забезпечення.</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;Повідомляйте про вади і ваші побажання&lt;/h1&gt; &lt;p&gt;Програмне забезпечення завжди потребує вдосконалення, і команда KDE готова це робити. Проте, вам (користувачеві) варто повідомити нам, якщо щось не працює, як слід, або щось можна покращити.&lt;/p&gt; &lt;p&gt;KDE має систему стеження за вадами. Завітайте на сторінку &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; , щоб повідомити розробників про ваду у програмі.&lt;/p&gt;Якщо у вас є пропозиція щодо вдосконалення, за допомогою цієї системи можна зареєструвати ваше побажання. Переконайтеся, що поле «Важливість» встановлено у значення «Список побажань» («Wishlist»).</string>
<string name="about_kde_join_kde">&lt;h1&gt;Долучайтеся до KDE&lt;/h1&gt; &lt;p&gt;Не обов\'язково бути програмістом, щоб належати до Команди KDE. Можете приєднатися до команд, що перекладають інтерфейс програм. Можете забезпечувати користувачів графікою, темами, звуками та вдосконаленою документацію. Вам вирішувати!&lt;/p&gt; &lt;p&gt;Завітайте на сторінку &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; щодо інформації про деякі проєкти, у яких можна взяти участь.&lt;/p&gt;Якщо ви потребуєте більше інформації або документації, відвідайте &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt;, щоб ознайомитися з нею.</string>

View File

@@ -381,6 +381,8 @@
<string name="send_compose">发送</string>
<string name="compose_send_title">编写发送</string>
<string name="open_compose_send">编写文本</string>
<string name="double_tap_to_drag">双击进行拖动</string>
<string name="hold_to_drag">按住进行拖动</string>
<string name="about_kde_about">&lt;h1&gt;关于&lt;/h1&gt; &lt;p&gt;KDE 是由一群致力于&lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;自由软件&lt;/a&gt;事业的人们所组成的全球性协作社区。它的成员包括了来自世界各地的软件工程师、艺术工作者、文字工作者、翻译人员和其他创意人员。KDE 社区开发了 Plasma 桌面环境、数百款功能各异的应用软件、以及用于支持它们的大量程序库。&lt;/p&gt; &lt;p&gt;KDE 是一项立足于协作精神的事业,它的运作和产出不受任何单一个人或者机构的控制。我们的共同目标是为全世界带来高品质的自由软件。不管您来自何方,我们都欢迎您&lt;a href=https://community.kde.org/Get_Involved&gt;加入 KDE 并做出贡献&lt;/a&gt;&lt;/p&gt;请访问 &lt;a href=https://www.kde.org/&gt;https://www.kde.org/&lt;/a&gt; 来了解 KDE 社区和软件的更多信息。</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;报告程序缺陷和需求&lt;/h1&gt; &lt;p&gt;KDE 团队一直致力于改进软件的品质。为了做到这一点,倾听来自用户的反馈非常重要。如果您遇到了软件不能正常工作的情况,请务必告诉我们。如果您有关于改进软件的想法,也请与我们分享。&lt;/p&gt; &lt;p&gt;KDE 建有程序缺陷跟踪系统,请访问 &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; 或者使用“帮助”菜单中的“报告缺陷”对话框填写报告。&lt;/p&gt;如果您想要提出改进建议而不是报告程序缺陷,请确保在表格的 Severity (严重程度) 选单中选择“Wishlist (需求)”。</string>
<string name="about_kde_join_kde">&lt;h1&gt;加入 KDE&lt;/h1&gt; &lt;p&gt;即使您不是软件开发人员,我们也欢迎您加入 KDE 的队伍!您可以加入各种语言的翻译团队来翻译软件的界面,您还可以制作图像、主题、音效,或者改进软件配套的文档……您的角色您来定!&lt;/p&gt; &lt;p&gt;请访问 &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; 来物色您感兴趣的项目。&lt;/p&gt;如需了解更多相关信息和文档,请访问 &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt;</string>

View File

@@ -374,11 +374,15 @@
<string name="kde_be_free">KDE ─ 擁抱自由!</string>
<string name="kde">KDE</string>
<string name="konqi">Konqi</string>
<string name="rise_up">往上</string>
<string name="rise_down">往下</string>
<string name="click_here_to_type">按這裡來打字</string>
<string name="clear_compose">清除</string>
<string name="send_compose">傳送</string>
<string name="compose_send_title">撰寫傳送</string>
<string name="open_compose_send">撰寫簡訊</string>
<string name="double_tap_to_drag">雙擊來拖曳</string>
<string name="hold_to_drag">按住來拖曳</string>
<string name="about_kde_about">&lt;h1&gt;關於&lt;/h1&gt;&lt;p&gt;KDE 是一個由眾多軟體工程師、藝術作業者、文字作業者、翻譯人員和其他有志於&lt;a href=https://www.gnu.org/philosophy/free-sw.html&gt;自由軟體&lt;/a&gt;事業的人們組成的世界性社群。KDE 做出了 Plasma 桌面環境、數百個應用程式和支援它們的軟體函式庫。&lt;/p&gt;&lt;p&gt;KDE 團隊不受任何單獨的團體、公司或機構控制。我們的目標是一同為製作世界上最好的自由軟體而努力。我們歡迎任何人&lt;a href=https://community.kde.org/Get_Involved&gt;加入並作出貢獻&lt;/a&gt;——包含您。&lt;/p&gt;請造訪 &lt;a href=&amp;quot;https://www.kde.org/&amp;quot;&gt;https://www.kde.org/&lt;/a&gt; 來了解有關 KDE 社群與我們所做的軟體的更多資訊。</string>
<string name="about_kde_report_bugs_or_wishes">&lt;h1&gt;回報問題或希望功能&lt;/h1&gt;&lt;p&gt;軟體總是可以改善,而 KDE 團隊準備好繼續此項工作。不過您 — 作為使用者 — 需要在有東西沒有正常運作或是可以更好的時候告訴我們。&lt;/p&gt;&lt;p&gt;KDE 有一個問題追蹤系統。請造訪 &lt;a href=https://bugs.kde.org/&gt;https://bugs.kde.org/&lt;/a&gt; 或是從關於頁面使用「回報問題」按鈕來回報問題。&lt;/p&gt;如果您有改善的建議您也可以使用問題追蹤系統來寫下您的希望。此時請使用「Wishlist」願望清單作為問題的嚴重程度。</string>
<string name="about_kde_join_kde">&lt;h1&gt;加入 KDE&lt;/h1&gt;&lt;p&gt;您不需要是軟體工程師也能加入 KDE 團隊。您可以加入翻譯團隊來翻譯程式的介面。您可以提供圖形、佈景主題、音效,或幫忙撰寫文件,看您喜歡做哪一項。&lt;/p&gt;&lt;p&gt;請參考 &lt;a href=https://community.kde.org/Get_Involved&gt;https://community.kde.org/Get_Involved&lt;/a&gt; 上的資訊看看您能幫上什麼忙。&lt;/p&gt;如果您需要更多資訊或說明文件,請參照 &lt;a href=https://techbase.kde.org/&gt;https://techbase.kde.org/&lt;/a&gt;,上面會有一些能幫忙您的資訊。</string>
@@ -393,6 +397,7 @@
<string name="maxim_leshchenko_task">使用者介面的改善,以及這個關於頁面</string>
<string name="holger_kaelberer_task">遠端鍵盤外掛程式與問題修正</string>
<string name="saikrishna_arcot_task">在遠端輸入外掛程式中對於鍵盤的支援、錯誤修正與一般改進</string>
<string name="shellwen_chen_task">改善 SFTP 的安全性、改善本專案的易維護性、錯誤修正與一般改進</string>
<string name="everyone_else">其他所有這些年來向 KDE 連線貢獻的人們</string>
<string name="send_clipboard">傳送剪貼簿</string>
<string name="tap_to_execute">按一下執行</string>

View File

@@ -85,6 +85,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<string name="mousepad_scroll_sensitivity_title">Scroll sensitivity</string>
<string name="gyro_mouse_enabled" translatable="false">gyro_mouse_enabled</string>
<string name="mousepad_mouse_buttons_enabled_pref" translatable="false">mouse_buttons_enabled</string>
<string name="mousepad_doubletap_drag_enabled_pref" translatable="false">doubletap_drag_enabled</string>
<string name="gyro_mouse_enabled_title">Enable gyroscope mouse</string>
<string name="gyro_mouse_sensitivity_title">Gyroscope sensitivity</string>
<string name="gyro_mouse_sensitivity" translatable="false">gyro_mouse_sensitivity</string>
@@ -513,6 +514,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<string name="send_compose">Send</string>
<string name="compose_send_title">Compose send</string>
<string name="open_compose_send">Compose text</string>
<string name="double_tap_to_drag">Double tap to drag</string>
<string name="hold_to_drag">Hold to drag</string>
<string name="about_kde_about"><![CDATA[
<h1>About</h1>

View File

@@ -90,6 +90,15 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
android:key="@string/mousepad_mouse_buttons_enabled_pref"
android:title="@string/mousepad_mouse_buttons_title" />
<SwitchPreference
android:id="@+id/mousepad_double_tap_drag_enabled_pref"
android:defaultValue="true"
android:key="@string/mousepad_doubletap_drag_enabled_pref"
android:title="Drag and drop behavior"
android:summaryOn="@string/double_tap_to_drag"
android:summaryOff="@string/hold_to_drag"
/>
<org.kde.kdeconnect.Helpers.LongSummaryPreferenceCategory
android:key="@string/sendkeystrokes_pref_category"

View File

@@ -109,7 +109,14 @@ public class LanLinkProvider extends BaseLinkProvider {
}
Log.i("KDE/LanLinkProvider", "identity packet received from a TCP connection from " + networkPacket.getString("deviceName"));
identityPacketReceived(networkPacket, socket, LanLink.ConnectionStarted.Locally);
boolean deviceTrusted = isDeviceTrusted(networkPacket.getString("deviceId"));
if (!deviceTrusted && !TrustedNetworkHelper.isTrustedNetwork(context)) {
Log.i("KDE/LanLinkProvider", "Ignoring identity packet because the device is not trusted and I'm not on a trusted network.");
return;
}
identityPacketReceived(networkPacket, socket, LanLink.ConnectionStarted.Locally, deviceTrusted);
}
//I've received their broadcast and should connect to their TCP socket and send my identity.
@@ -149,6 +156,12 @@ public class LanLinkProvider extends BaseLinkProvider {
Log.i("KDE/LanLinkProvider", "Broadcast identity packet received from " + identityPacket.getString("deviceName"));
boolean deviceTrusted = isDeviceTrusted(identityPacket.getString("deviceId"));
if (!deviceTrusted && !TrustedNetworkHelper.isTrustedNetwork(context)) {
Log.i("KDE/LanLinkProvider", "Ignoring identity packet because the device is not trusted and I'm not on a trusted network.");
return;
}
SocketFactory socketFactory = SocketFactory.getDefault();
Socket socket = socketFactory.createSocket(address, tcpPort);
configureSocket(socket);
@@ -160,7 +173,7 @@ public class LanLinkProvider extends BaseLinkProvider {
out.write(myIdentity.serialize().getBytes());
out.flush();
identityPacketReceived(identityPacket, socket, LanLink.ConnectionStarted.Remotely);
identityPacketReceived(identityPacket, socket, LanLink.ConnectionStarted.Remotely, deviceTrusted);
}
private void configureSocket(Socket socket) {
@@ -171,6 +184,11 @@ public class LanLinkProvider extends BaseLinkProvider {
}
}
private boolean isDeviceTrusted(String deviceId) {
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
return preferences.getBoolean(deviceId, false);
}
/**
* Called when a new 'identity' packet is received. Those are passed here by
* {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}.
@@ -184,9 +202,10 @@ public class LanLinkProvider extends BaseLinkProvider {
* @param identityPacket identity of a remote device
* @param socket a new Socket, which should be used to receive packets from the remote device
* @param connectionStarted which side started this connection
* @param deviceTrusted whether the packet comes from a trusted device
*/
@WorkerThread
private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted) throws IOException {
private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final LanLink.ConnectionStarted connectionStarted, final boolean deviceTrusted) throws IOException {
if (!DeviceInfo.isValidIdentityPacket(identityPacket)) {
Log.w("KDE/LanLinkProvider", "Invalid identity packet received.");
@@ -203,10 +222,7 @@ public class LanLinkProvider extends BaseLinkProvider {
// If I'm the TCP server I will be the SSL client and viceversa.
final boolean clientMode = (connectionStarted == LanLink.ConnectionStarted.Locally);
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
boolean isDeviceTrusted = preferences.getBoolean(deviceId, false);
if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) {
if (deviceTrusted && !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) {
@@ -214,13 +230,13 @@ public class LanLinkProvider extends BaseLinkProvider {
}
device.unpair();
//Retry as unpaired
identityPacketReceived(identityPacket, socket, connectionStarted);
identityPacketReceived(identityPacket, socket, connectionStarted, deviceTrusted);
}
String deviceName = identityPacket.getString("deviceName", "unknown");
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + deviceName + " trusted:" + isDeviceTrusted);
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + deviceName + " trusted:" + deviceTrusted);
final SSLSocket sslSocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode);
final SSLSocket sslSocket = SslHelper.convertToSslSocket(context, socket, deviceId, deviceTrusted, clientMode);
sslSocket.addHandshakeCompletedListener(event -> {
String mode = clientMode ? "client" : "server";
try {

View File

@@ -1,91 +0,0 @@
/*
* 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.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.LinkedList;
public class NsdResolveQueue {
static final String LOG_TAG = "NsdResolveQueue";
final @NonNull NsdManager mNsdManager;
private final Object mLock = new Object();
private final LinkedList<PendingResolve> mResolveRequests = new LinkedList<>();
public NsdResolveQueue(NsdManager nsdManager) {
this.mNsdManager = nsdManager;
}
private static class PendingResolve {
final @NonNull NsdServiceInfo serviceInfo;
final @NonNull NsdManager.ResolveListener listener;
private PendingResolve(@NonNull NsdServiceInfo serviceInfo, @NonNull NsdManager.ResolveListener listener) {
this.serviceInfo = serviceInfo;
this.listener = listener;
}
}
public void resolveOrEnqueue(@NonNull NsdServiceInfo serviceInfo, @NonNull NsdManager.ResolveListener listener) {
synchronized (mLock) {
for (PendingResolve existing : mResolveRequests) {
if (serviceInfo.getServiceName().equals(existing.serviceInfo.getServiceName())) {
Log.i(LOG_TAG, "Not enqueuing a new resolve request for the same service: " + serviceInfo.getServiceName());
return;
}
}
mResolveRequests.addLast(new PendingResolve(serviceInfo, new ListenerWrapper(listener)));
if (mResolveRequests.size() == 1) {
resolveNextRequest();
}
}
}
private class ListenerWrapper implements NsdManager.ResolveListener {
private final @NonNull NsdManager.ResolveListener mListener;
private ListenerWrapper(@NonNull NsdManager.ResolveListener listener) {
mListener = listener;
}
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
mListener.onResolveFailed(serviceInfo, errorCode);
synchronized (mLock) {
mResolveRequests.pop();
resolveNextRequest();
}
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
mListener.onServiceResolved(serviceInfo);
synchronized (mLock) {
mResolveRequests.pop();
resolveNextRequest();
}
}
}
private void resolveNextRequest() {
if (!mResolveRequests.isEmpty()) {
PendingResolve request = mResolveRequests.getFirst();
mNsdManager.resolveService(request.serviceInfo, request.listener);
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.util.Log
import java.util.LinkedList
class NsdResolveQueue {
val LOG_TAG: String = "NsdResolveQueue"
private val nsdManager: NsdManager
private val lock: Any
private data class PendingResolve(val serviceInfo: NsdServiceInfo, val listener: NsdManager.ResolveListener)
private val resolveRequests: LinkedList<PendingResolve>
constructor(nsdManager: NsdManager) {
this.nsdManager = nsdManager
this.lock = Any()
this.resolveRequests = LinkedList<PendingResolve>()
}
fun resolveOrEnqueue(serviceInfo: NsdServiceInfo, listener: NsdManager.ResolveListener) {
synchronized(lock) {
if (resolveRequests.any { r -> serviceInfo.serviceName == r.serviceInfo.serviceName }) {
Log.i(LOG_TAG, "Not enqueuing a new resolve request for the same service: " + serviceInfo.serviceName)
return
}
resolveRequests.addLast(PendingResolve(serviceInfo, ListenerWrapper(listener)))
if (resolveRequests.size == 1) {
resolveNextRequest()
}
}
}
private inner class ListenerWrapper(private val listener: NsdManager.ResolveListener) : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
listener.onResolveFailed(serviceInfo, errorCode)
postResolve()
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
listener.onServiceResolved(serviceInfo)
postResolve()
}
private fun postResolve() {
synchronized(lock) {
resolveRequests.pop()
resolveNextRequest()
}
}
}
private fun resolveNextRequest() {
if (resolveRequests.isNotEmpty()) {
val request = resolveRequests.first
nsdManager.resolveService(request.serviceInfo, request.listener)
}
}
}

View File

@@ -1,50 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 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.LoopbackBackend;
import android.content.Context;
import androidx.annotation.NonNull;
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, linkProvider);
}
@Override
public String getName() {
return "LoopbackLink";
}
@WorkerThread
@Override
public boolean sendPacket(@NonNull NetworkPacket in, @NonNull Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {
packetReceived(in);
if (in.hasPayload()) {
callback.onPayloadProgressChanged(0);
in.setPayload(in.getPayload());
callback.onPayloadProgressChanged(100);
}
callback.onSuccess();
return true;
}
@Override
public DeviceInfo getDeviceInfo() {
return DeviceHelper.getDeviceInfo(context);
}
}

View File

@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2014 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.LoopbackBackend
import android.content.Context
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.getDeviceInfo
import org.kde.kdeconnect.NetworkPacket
class LoopbackLink : BaseLink {
constructor(context: Context, linkProvider: BaseLinkProvider) : super(context, linkProvider)
override fun getName(): String = "LoopbackLink"
override fun getDeviceInfo(): DeviceInfo = getDeviceInfo(context)
@WorkerThread
override fun sendPacket(packet: NetworkPacket, callback: Device.SendPacketStatusCallback, sendPayloadFromSameThread: Boolean): Boolean {
packetReceived(packet)
if (packet.hasPayload()) {
callback.onPayloadProgressChanged(0)
packet.payload = packet.payload // this triggers logic in the setter
callback.onPayloadProgressChanged(100)
}
callback.onSuccess()
return true
}
}

View File

@@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 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.LoopbackBackend;
import android.content.Context;
import android.net.Network;
import androidx.annotation.Nullable;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
public class LoopbackLinkProvider extends BaseLinkProvider {
private final Context context;
public LoopbackLinkProvider(Context context) {
this.context = context;
}
@Override
public void onStart() {
onNetworkChange(null);
}
@Override
public void onStop() {
}
@Override
public void onNetworkChange(@Nullable Network network) {
LoopbackLink link = new LoopbackLink(context, this);
onConnectionReceived(link);
}
@Override
public String getName() {
return "LoopbackLinkProvider";
}
@Override
public int getPriority() { return 0; }
}

View File

@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2014 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.LoopbackBackend
import android.content.Context
import android.net.Network
import org.kde.kdeconnect.Backends.BaseLinkProvider
class LoopbackLinkProvider : BaseLinkProvider {
private val context: Context
constructor(context: Context) : super() {
this.context = context
}
override fun getName(): String = "LoopbackLinkProvider"
override fun getPriority(): Int = 0
override fun onStart() {
onNetworkChange(null)
}
override fun onStop() { }
override fun onNetworkChange(network: Network?) {
val link = LoopbackLink(context, this)
onConnectionReceived(link)
}
}

View File

@@ -112,7 +112,7 @@ class Device : PacketReceiver {
this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE)
this.deviceInfo = loadFromSettings(context, deviceId, settings)
this.pairingHandler = PairingHandler(this, createDefaultPairingCallback(), PairingHandler.PairState.Paired)
this.supportedPlugins = Vector(PluginFactory.getAvailablePlugins()) // Assume all are supported until we receive capabilities
this.supportedPlugins = Vector(PluginFactory.availablePlugins) // Assume all are supported until we receive capabilities
Log.i("Device", "Loading trusted device: ${deviceInfo.name}")
}
@@ -126,7 +126,7 @@ class Device : PacketReceiver {
this.deviceInfo = link.deviceInfo
this.settings = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE)
this.pairingHandler = PairingHandler(this, createDefaultPairingCallback(), PairingHandler.PairState.NotPaired)
this.supportedPlugins = Vector(PluginFactory.getAvailablePlugins()) // Assume all are supported until we receive capabilities
this.supportedPlugins = Vector(PluginFactory.availablePlugins) // Assume all are supported until we receive capabilities
Log.i("Device", "Creating untrusted device: " + deviceInfo.name)
addLink(link)
}
@@ -622,7 +622,7 @@ class Device : PacketReceiver {
supportedPlugins.forEach { pluginKey ->
val pluginInfo = PluginFactory.getPluginInfo(pluginKey)
val listenToUnpaired = pluginInfo.listenToUnpaired()
val listenToUnpaired = pluginInfo.listenToUnpaired
val pluginEnabled = (isPaired || listenToUnpaired) && this.isReachable && isPluginEnabled(pluginKey)

View File

@@ -160,8 +160,8 @@ object DeviceHelper {
getDeviceName(context),
deviceType,
ProtocolVersion,
PluginFactory.getIncomingCapabilities(),
PluginFactory.getOutgoingCapabilities()
PluginFactory.incomingCapabilities,
PluginFactory.outgoingCapabilities
)
}

View File

@@ -1,251 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 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.Helpers;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.util.Log;
import android.webkit.MimeTypeMap;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.kde.kdeconnect.NetworkPacket;
import java.io.File;
import java.io.InputStream;
import java.util.Arrays;
public class FilesHelper {
public static final String LOG_TAG = "SendFileActivity";
public static String getMimeTypeFromFile(String file) {
String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file));
return StringUtils.defaultString(mime, "*/*");
}
public static String findNonExistingNameForNewFile(String path, String filename) {
String name = FilenameUtils.getBaseName(filename);
String ext = FilenameUtils.getExtension(filename);
int num = 1;
while (new File(path + "/" + filename).exists()) {
filename = name + " (" + num + ")." + ext;
num++;
}
return filename;
}
//Following code from http://activemq.apache.org/maven/5.7.0/kahadb/apidocs/src-html/org/apache/kahadb/util/IOHelper.html
/**
* Converts any string into a string that is safe to use as a file name.
* The result will only include ascii characters and numbers, and the "-","_", and "." characters.
*/
private static String toFileSystemSafeName(String name, boolean dirSeparators, int maxFileLength) {
int size = name.length();
StringBuilder rc = new StringBuilder(size * 2);
for (int i = 0; i < size; i++) {
char c = name.charAt(i);
boolean valid = c >= 'a' && c <= 'z';
valid = valid || (c >= 'A' && c <= 'Z');
valid = valid || (c >= '0' && c <= '9');
valid = valid || (c == '_') || (c == '-') || (c == '.');
valid = valid || (dirSeparators && ((c == '/') || (c == '\\')));
if (valid) {
rc.append(c);
}
}
String result = rc.toString();
if (result.length() > maxFileLength) {
result = result.substring(result.length() - maxFileLength);
}
return result;
}
public static String toFileSystemSafeName(String name, boolean dirSeparators) {
return toFileSystemSafeName(name, dirSeparators, 255);
}
public static String toFileSystemSafeName(String name) {
return toFileSystemSafeName(name, true, 255);
}
private static int GetOpenFileCount() {
return new File("/proc/self/fd").listFiles().length;
}
public static void LogOpenFileCount() {
Log.e("KDE/FileCount", "" + GetOpenFileCount());
}
//Create the network packet from the URI
public static NetworkPacket uriToNetworkPacket(final Context context, final Uri uri, String type) {
try {
ContentResolver cr = context.getContentResolver();
InputStream inputStream = cr.openInputStream(uri);
NetworkPacket np = new NetworkPacket(type);
String filename = null;
long size = -1;
Long lastModified = null;
if (uri.getScheme().equals("file")) {
// file:// is a non media uri, so we cannot query the ContentProvider
try {
File mFile = new File(uri.getPath());
filename = mFile.getName();
size = mFile.length();
lastModified = mFile.lastModified();
} catch (NullPointerException e) {
Log.e(LOG_TAG, "Received bad file URI", e);
}
} else {
// Since we used Intent.CATEGORY_OPENABLE, these two columns are the only ones we are
// guaranteed to have: https://developer.android.com/reference/android/provider/OpenableColumns
String[] proj = {
OpenableColumns.SIZE,
OpenableColumns.DISPLAY_NAME,
};
try (Cursor cursor = cr.query(uri, proj, null, null, null)) {
int nameColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME);
int sizeColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE);
cursor.moveToFirst();
filename = cursor.getString(nameColumnIndex);
// It is recommended to check for the value to be null because there are
// situations were we don't know the size (for instance, if the file is
// not local to the device)
if (!cursor.isNull(sizeColumnIndex)) {
size = cursor.getLong(sizeColumnIndex);
}
lastModified = getLastModifiedTime(context, uri);
} catch (Exception e) {
Log.e(LOG_TAG, "Problem getting file information", e);
}
}
if (filename != null) {
np.set("filename", filename);
} else {
// It would be very surprising if this happens
Log.e(LOG_TAG, "Unable to read filename");
}
if (lastModified != null) {
np.set("lastModified", lastModified);
} else {
// This would not be too surprising, and probably means we need to improve
// FilesHelper.getLastModifiedTime
Log.w(LOG_TAG, "Unable to read file last modified time");
}
np.setPayload(new NetworkPacket.Payload(inputStream, size));
return np;
} catch (Exception e) {
Log.e(LOG_TAG, "Exception creating network packet", e);
return null;
}
}
/**
* By hook or by crook, get the last modified time of the passed content:// URI
*
* This is a challenge because different content sources have different columns defined, and
* I don't know how to tell what the source of the content is.
*
* Therefore, my brilliant solution is to just try everything until something works.
*
* Will return null if nothing worked.
*/
public static Long getLastModifiedTime(final Context context, final Uri uri) {
ContentResolver cr = context.getContentResolver();
Long lastModifiedTime = null;
// Open a cursor without a column because we do not yet know what columns are defined
try (Cursor cursor = cr.query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
String[] allColumns = cursor.getColumnNames();
// MediaStore.MediaColumns.DATE_MODIFIED resolves to "date_modified"
// I see this column defined in case we used the Gallery app to select the file to transfer
// This can occur both for devices running Storage Access Framework (SAF) if we select
// the Gallery to provide the file to transfer, as well as for older devices by doing the same
int mediaDataModifiedColumnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED);
// DocumentsContract.Document.COLUMN_LAST_MODIFIED resolves to "last_modified"
// I see this column defined when, on a device using SAF we select a file using the
// file browser
// According to https://developer.android.com/reference/kotlin/android/provider/DocumentsContract
// all "document providers" must provide certain columns. Do we actually have a DocumentProvider here?
// I do not think this code path will ever happen for a non-media file is selected on
// an API < KitKat device, since those will be delivered as a file:// URI and handled
// accordingly. Therefore, it is safe to ignore the warning that this field requires
// API 19
@SuppressLint("InlinedApi")
int documentLastModifiedColumnIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED);
// If we have an image, it may be the case that MediaStore.MediaColumns.DATE_MODIFIED
// catches the modification date, but if not, here is another column we can look for.
// This should be checked *after* DATE_MODIFIED since I think that column might give
// better information
int imageDateTakenColumnIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);
// Report whether the captured timestamp is in milliseconds or seconds
// The truthy-ness of this value for each different type of column is known from either
// experimentation or the docs (when docs exist...)
boolean milliseconds;
int properColumnIndex;
if (mediaDataModifiedColumnIndex >= 0) {
properColumnIndex = mediaDataModifiedColumnIndex;
milliseconds = false;
} else if (documentLastModifiedColumnIndex >= 0) {
properColumnIndex = documentLastModifiedColumnIndex;
milliseconds = true;
} else if (imageDateTakenColumnIndex >= 0) {
properColumnIndex = imageDateTakenColumnIndex;
milliseconds = true;
} else {
// Nothing worked :(
String formattedColumns = Arrays.toString(allColumns);
Log.w("SendFileActivity", "Unable to get file modification time. Available columns were: " + formattedColumns);
return null;
}
if (!cursor.isNull(properColumnIndex)) {
lastModifiedTime = cursor.getLong(properColumnIndex);
}
if (!milliseconds) {
lastModifiedTime *= 1000;
milliseconds = true;
}
}
}
return lastModifiedTime;
}
}

View File

@@ -0,0 +1,206 @@
/*
* SPDX-FileCopyrightText: 2014 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.Helpers
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
import android.webkit.MimeTypeMap
import org.apache.commons.io.FilenameUtils
import org.kde.kdeconnect.NetworkPacket
import java.io.File
import kotlin.math.min
object FilesHelper {
private const val LOG_TAG: String = "SendFileActivity"
@JvmStatic
fun getMimeTypeFromFile(file: String?): String {
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file))
return mime ?: "*/*"
}
@JvmStatic
fun findNonExistingNameForNewFile(path: String, filename: String): String {
var newFilename = filename
val name = FilenameUtils.getBaseName(newFilename)
val ext = FilenameUtils.getExtension(newFilename)
var num = 1
while (File("$path/$newFilename").exists()) {
newFilename = "$name ($num).$ext"
num++
}
return newFilename
}
/**
* Converts any string into a string that is safe to use as a file name.
* The result will only include ascii characters and numbers, and the "-","_", and "." characters.
*/
private fun toFileSystemSafeName(name: String, dirSeparators: Boolean, maxFileLength: Int): String {
fun isSafeChar(c: Char): Boolean =
c in 'a'..'z' || c in 'A'..'Z' || c in '0'..'9' ||
c == '_' || c == '-' || c == '.' ||
(dirSeparators && ((c == '/') || (c == '\\')))
val nameSafeChars = name.filter(::isSafeChar)
val nameSafeLength = nameSafeChars.substring(nameSafeChars.length - min(nameSafeChars.length, maxFileLength))
return nameSafeLength
}
fun toFileSystemSafeName(name: String, dirSeparators: Boolean): String = toFileSystemSafeName(name, dirSeparators, 255)
fun toFileSystemSafeName(name: String): String = toFileSystemSafeName(name, true, 255)
private fun getOpenFileCount(): Int? = File("/proc/self/fd").listFiles()?.size
fun LogOpenFileCount() {
Log.e("KDE/FileCount", "" + getOpenFileCount())
}
/**
* Creates a network packet from the given URI
*/
@JvmStatic
fun uriToNetworkPacket(context: Context, uri: Uri, type: String?): NetworkPacket? {
try {
val contentResolver = context.contentResolver
val inputStream = contentResolver.openInputStream(uri)
val packet = NetworkPacket(type!!)
val sizeDefault = -1L
// file:// is a non media uri, so we cannot query the ContentProvider
fun fileSchemeExtract(): Triple<String?, Long, Long?> {
val path = uri.path
if (path != null) {
val mFile = File(path)
val filename = mFile.name
val size = mFile.length()
val lastModified = mFile.lastModified()
return Triple(filename, size, lastModified)
}
else {
Log.e(LOG_TAG, "Received bad file URI, path was null")
return Triple(null, sizeDefault, null)
}
}
fun contentResolverExtract(): Triple<String?, Long, Long?> {
// Since we used Intent.CATEGORY_OPENABLE, these two columns are the only ones we are guaranteed to have: https://developer.android.com/reference/android/provider/OpenableColumns
val proj = arrayOf(OpenableColumns.SIZE, OpenableColumns.DISPLAY_NAME,)
try {
contentResolver.query(uri, proj, null, null, null).use { cursor ->
val nameColumnIndex = cursor!!.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
val sizeColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
cursor.moveToFirst()
val filename = cursor.getString(nameColumnIndex)
// It is recommended to check for the value to be null because there are situations were we don't know the size (for instance, if the file is not local to the device)
val size = if (!cursor.isNull(sizeColumnIndex)) cursor.getLong(sizeColumnIndex) else sizeDefault
val lastModified = getLastModifiedTime(context, uri)
return Triple(filename, size, lastModified)
}
}
catch (e: Exception) {
Log.e(LOG_TAG, "Problem getting file information", e)
}
return Triple(null, sizeDefault, null)
}
val (filename, size, lastModified) = when (uri.scheme) {
"file" -> fileSchemeExtract()
else -> contentResolverExtract()
}
if (filename != null) {
packet["filename"] = filename
}
else {
// It would be very surprising if this happens
Log.e(LOG_TAG, "Unable to read filename")
}
if (lastModified != null) {
packet["lastModified"] = lastModified
}
else {
// This would not be too surprising, and probably means we need to improve FilesHelper.getLastModifiedTime
Log.w(LOG_TAG, "Unable to read file last modified time")
}
packet.payload = NetworkPacket.Payload(inputStream, size)
return packet
}
catch (e: Exception) {
Log.e(LOG_TAG, "Exception creating network packet", e)
return null
}
}
/**
* By hook or by crook, get the last modified time of the passed content:// URI
*
* This is a challenge because different content sources have different columns defined, and
* I don't know how to tell what the source of the content is.
*
* Therefore, my brilliant solution is to just try everything until something works.
*
* Will return null if nothing worked.
*
* @return Time last modified in milliseconds or null
*/
fun getLastModifiedTime(context: Context, uri: Uri): Long? {
context.contentResolver.query(uri, null, null, null, null).use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
val allColumns = cursor.columnNames
// MediaStore.MediaColumns.DATE_MODIFIED resolves to "date_modified"
// I see this column defined in case we used the Gallery app to select the file to transfer
// This can occur both for devices running Storage Access Framework (SAF) if we select the Gallery to provide the file to transfer, as well as for older devices by doing the same
val mediaDataModifiedColumnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED)
// DocumentsContract.Document.COLUMN_LAST_MODIFIED resolves to "last_modified"
// I see this column defined when, on a device using SAF we select a file using the file browser
// According to https://developer.android.com/reference/kotlin/android/provider/DocumentsContract all "document providers" must provide certain columns.
// Do we actually have a DocumentProvider here?
// I do not think this code path will ever happen for a non-media file is selected on an API < KitKat device, since those will be delivered as a file:// URI and handled accordingly.
// Therefore, it is safe to ignore the warning that this field requires API 19
@SuppressLint("InlinedApi") val documentLastModifiedColumnIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
// If we have an image, it may be the case that MediaStore.MediaColumns.DATE_MODIFIED catches the modification date, but if not, here is another column we can look for.
// This should be checked *after* DATE_MODIFIED since I think that column might give better information
val imageDateTakenColumnIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN)
// Report whether the captured timestamp is in milliseconds or seconds
// The truthy-ness of this value for each different type of column is known from either experimentation or the docs (when docs exist...)
val (properColumnIndex: Int, milliseconds: Boolean) = when {
mediaDataModifiedColumnIndex >= 0 -> Pair(mediaDataModifiedColumnIndex, false)
documentLastModifiedColumnIndex >= 0 -> Pair(documentLastModifiedColumnIndex, true)
imageDateTakenColumnIndex >= 0 -> Pair(imageDateTakenColumnIndex, true)
else -> {
// Nothing worked :(
Log.w("SendFileActivity", "Unable to get file modification time. Available columns were: ${allColumns.contentToString()}")
return null
}
}
val lastModifiedTime: Long? = if (!cursor.isNull(properColumnIndex)) cursor.getLong(properColumnIndex) else null
return if (!milliseconds) lastModifiedTime!! * 1000 else lastModifiedTime
}
}
return null
}
}

View File

@@ -1,23 +0,0 @@
/*
* 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.Helpers;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
public class MediaStoreHelper {
//Maybe this class could batch successive calls together
public static void indexFile(Context context, Uri path) {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(path);
context.sendBroadcast(mediaScanIntent);
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.Helpers
import android.content.Context
import android.content.Intent
import android.net.Uri
object MediaStoreHelper {
// Maybe this class could batch successive calls together
@JvmStatic
fun indexFile(context: Context, path: Uri?) {
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
mediaScanIntent.setData(path)
context.sendBroadcast(mediaScanIntent)
}
}

View File

@@ -1,28 +0,0 @@
/*
* 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.Helpers;
import java.security.SecureRandom;
public class RandomHelper {
public static final SecureRandom secureRandom = new SecureRandom();
private static final char[] symbols = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"abcdefghijklmnopqrstuvwxyz" +
"1234567890").toCharArray();
public static String randomString(int length) {
char[] buffer = new char[length];
for (int idx = 0; idx < length; ++idx) {
buffer[idx] = symbols[secureRandom.nextInt(symbols.length)];
}
return new String(buffer);
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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.Helpers
import java.security.SecureRandom
object RandomHelper {
@JvmField
val secureRandom: SecureRandom = SecureRandom()
private val symbols = (('A'..'Z').toList() + ('a'..'z').toList() + ('0'..'9').toList()).toCharArray()
fun randomString(length: Int): String {
val buffer = CharArray(length)
for (i in 0 until length) {
buffer[i] = symbols[secureRandom.nextInt(symbols.size)]
}
return String(buffer)
}
}

View File

@@ -1,85 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015 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.Helpers.SecurityHelpers;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class RsaHelper {
public static void initialiseRsaKeys(Context context) {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
if (!settings.contains("publicKey") || !settings.contains("privateKey")) {
KeyPair keyPair;
String keyAlgorithm;
try {
KeyPairGenerator keyGen;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
keyAlgorithm = KeyProperties.KEY_ALGORITHM_EC;
keyGen = KeyPairGenerator.getInstance(keyAlgorithm);
ECGenParameterSpec spec = new ECGenParameterSpec("secp256r1");
keyGen.initialize(spec);
} else {
keyAlgorithm = "RSA";
keyGen = KeyPairGenerator.getInstance(keyAlgorithm);
keyGen.initialize(2048);
}
keyPair = keyGen.generateKeyPair();
} catch (Exception e) {
Log.e("KDE/initializeRsaKeys", "Exception", e);
return;
}
byte[] publicKey = keyPair.getPublic().getEncoded();
byte[] privateKey = keyPair.getPrivate().getEncoded();
SharedPreferences.Editor edit = settings.edit();
edit.putString("publicKey", Base64.encodeToString(publicKey, 0).trim() + "\n");
edit.putString("privateKey", Base64.encodeToString(privateKey, 0));
edit.putString("keyAlgorithm", keyAlgorithm);
edit.apply();
}
}
public static PublicKey getPublicKey(Context context) throws GeneralSecurityException {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
byte[] publicKeyBytes = Base64.decode(settings.getString("publicKey", ""), 0);
// For backwards compat: if no keyAlgorithm setting is set, it means it was generated using RSA
String keyAlgorithm = settings.getString("keyAlgorithm", "RSA");
return KeyFactory.getInstance(keyAlgorithm).generatePublic(new X509EncodedKeySpec(publicKeyBytes));
}
public static PrivateKey getPrivateKey(Context context) throws GeneralSecurityException {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
byte[] privateKeyBytes = Base64.decode(settings.getString("privateKey", ""), 0);
// For backwards compat: if no keyAlgorithm setting is set, it means it was generated using RSA
String keyAlgorithm = settings.getString("keyAlgorithm", "RSA");
return KeyFactory.getInstance(keyAlgorithm).generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
}
}

View File

@@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2015 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.Helpers.SecurityHelpers
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.preference.PreferenceManager
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import java.security.GeneralSecurityException
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.ECGenParameterSpec
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
object RsaHelper {
private const val RSA = "RSA" // KeyProperties.KEY_ALGORITHM_RSA isn't available until API 23+
@JvmStatic
fun initialiseRsaKeys(context: Context?) {
val settings = PreferenceManager.getDefaultSharedPreferences(context)
if (!settings.contains("publicKey") || !settings.contains("privateKey")) {
val keyPair: KeyPair
val keyAlgorithm: String
try {
val newSdk = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
keyAlgorithm = if (newSdk) KeyProperties.KEY_ALGORITHM_EC else RSA
val generator = KeyPairGenerator.getInstance(keyAlgorithm)
if (newSdk) {
val spec = ECGenParameterSpec("secp256r1")
generator.initialize(spec)
}
else {
generator.initialize(2048)
}
keyPair = generator.generateKeyPair()
}
catch (e: Exception) {
Log.e("KDE/initializeRsaKeys", "Exception", e)
return
}
val publicKey = keyPair.public.encoded
val privateKey = keyPair.private.encoded
settings.edit().apply {
putString("publicKey", Base64.encodeToString(publicKey, 0))
putString("privateKey", Base64.encodeToString(privateKey, 0))
putString("keyAlgorithm", keyAlgorithm)
apply()
}
}
}
/** For backwards compat: if no keyAlgorithm setting is set, it means it was generated using RSA */
private fun algorithmFromSettings(pref: SharedPreferences) = pref.getString("keyAlgorithm", RSA)
@JvmStatic
@Throws(GeneralSecurityException::class)
fun getPublicKey(context: Context?): PublicKey {
val settings = PreferenceManager.getDefaultSharedPreferences(context)
val publicKeyBytes = Base64.decode(settings.getString("publicKey", ""), 0)
return KeyFactory.getInstance(algorithmFromSettings(settings)).generatePublic(X509EncodedKeySpec(publicKeyBytes))
}
@JvmStatic
@Throws(GeneralSecurityException::class)
fun getPrivateKey(context: Context?): PrivateKey {
val settings = PreferenceManager.getDefaultSharedPreferences(context)
val privateKeyBytes = Base64.decode(settings.getString("privateKey", ""), 0)
return KeyFactory.getInstance(algorithmFromSettings(settings)).generatePrivate(PKCS8EncodedKeySpec(privateKeyBytes))
}
}

View File

@@ -26,6 +26,7 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.Arrays;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.RandomHelper;
import org.kde.kdeconnect.KdeConnect;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -104,7 +105,9 @@ public class SslHelper {
boolean needsToGenerateCertificate = false;
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
if (settings.contains("certificate")) {
final Date currDate = new Date();
try {
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context);
byte[] certificateBytes = Base64.decode(globalSettings.getString("certificate", ""), 0);
@@ -114,7 +117,14 @@ public class SslHelper {
if (!certDeviceId.equals(deviceId)) {
Log.e("KDE/SslHelper", "The certificate stored is from a different device id! (found: " + certDeviceId + " expected:" + deviceId + ")");
needsToGenerateCertificate = true;
} else {
}
else if(cert.getNotAfter().getTime() < currDate.getTime()) {
Log.e("KDE/SslHelper", "The certificate expired: "+cert.getNotAfter());
needsToGenerateCertificate = true;
} else if(cert.getNotBefore().getTime() > currDate.getTime()) {
Log.e("KDE/SslHelper", "The certificate is not effective yet: "+cert.getNotBefore());
needsToGenerateCertificate = true;
}else {
certificate = cert;
}
} catch (Exception e) {
@@ -127,6 +137,7 @@ public class SslHelper {
}
if (needsToGenerateCertificate) {
KdeConnect.getInstance().removeRememberedDevices();
Log.i("KDE/SslHelper", "Generating a certificate");
try {
//Fix for https://issuetracker.google.com/issues/37095309

View File

@@ -8,140 +8,16 @@ package org.kde.kdeconnect.Helpers;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.util.Log;
import androidx.annotation.NonNull;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
//Code from http://stackoverflow.com/questions/9340332/how-can-i-get-the-list-of-mounted-external-storage-of-android-device/19982338#19982338
//modified to work on Lollipop and other devices
public class StorageHelper {
public static class StorageInfo {
public final String path;
public final boolean readonly;
public final boolean removable;
public final int number;
public StorageInfo(String path, boolean readonly, boolean removable, int number) {
this.path = path;
this.readonly = readonly;
this.removable = removable;
this.number = number;
}
}
/*
* This function is bullshit because there is no proper way to do this in Android.
* Patch after patch I'm making it even more horrible by trying to make it work for *more*
* devices while trying no to break previously working ones.
* If this function was a living being, it would be begging "please kill me".
*/
public static List<StorageInfo> getStorageList() {
List<StorageInfo> list = new ArrayList<>();
String def_path = Environment.getExternalStorageDirectory().getPath();
boolean def_path_removable = Environment.isExternalStorageRemovable();
String def_path_state = Environment.getExternalStorageState();
boolean def_path_available = def_path_state.equals(Environment.MEDIA_MOUNTED)
|| def_path_state.equals(Environment.MEDIA_MOUNTED_READ_ONLY);
boolean def_path_readonly = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY);
HashSet<String> paths = new HashSet<>();
int cur_removable_number = 1;
if (def_path_available) {
paths.add(def_path);
list.add(0, new StorageInfo(def_path, def_path_readonly, def_path_removable, def_path_removable ? cur_removable_number++ : -1));
}
File storage = new File("/storage/");
if (storage.exists() && storage.isDirectory() && storage.canRead()) {
String mounts = null;
try (Scanner scanner = new Scanner(new File("/proc/mounts"))) {
mounts = scanner.useDelimiter("\\A").next();
} catch (Exception e) {
Log.e("StorageHelper", "Exception while getting storageList", e);
}
File[] dirs = storage.listFiles();
for (File dir : dirs) {
//Log.e("getStorageList", "path: "+dir.getAbsolutePath());
if (dir.isDirectory() && dir.canRead() && dir.canExecute()) {
String path, path2;
path2 = dir.getAbsolutePath();
try {
//Log.e(dir.getAbsolutePath(), dir.getCanonicalPath());
path = dir.getCanonicalPath();
} catch (Exception e) {
path = path2;
}
if (!path.startsWith("/storage/emulated") || dirs.length == 1) {
if (!paths.contains(path) && !paths.contains(path2)) {
if (mounts == null || StringUtils.containsAny(mounts, path, path2)) {
list.add(0, new StorageInfo(path, dir.canWrite(), true, cur_removable_number++));
paths.add(path);
}
}
}
}
}
} else {
//Legacy code for Android < 4.0 that still didn't have /storage
List<String> entries = new ArrayList<>();
try (FileReader fileReader = new FileReader("/proc/mounts")) {
// The reader is buffered internally, so buffering it separately is unnecessary.
final List<String> lines = IOUtils.readLines(fileReader).stream()
.filter(line -> StringUtils.containsAny(line, "vfat", "exfat", "ntfs", "/mnt"))
.collect(Collectors.toList());
for (String line : lines) {
if (line.contains("/storage/sdcard"))
entries.add(0, line);
else
entries.add(line);
}
} catch (Exception e) {
Log.e("StorageHelper", "Exception", e);
}
for (String line : entries) {
StringTokenizer tokens = new StringTokenizer(line, " ");
tokens.nextToken(); //device
String mount_point = tokens.nextToken(); //mount point
if (paths.contains(mount_point)) {
continue;
}
tokens.nextToken(); //file system
String[] flags = tokens.nextToken().split(","); //flags
boolean readonly = ArrayUtils.contains(flags, "ro");
if (line.contains("/dev/block/vold") && !StringUtils.containsAny(line, "/mnt/secure",
"/mnt/asec", "/mnt/obb", "/dev/mapper", "tmpfs")) {
paths.add(mount_point);
list.add(new StorageInfo(mount_point, readonly, true, cur_removable_number++));
}
}
}
return list;
}
/* treeUri documentId
* ==================================================================================================
* content://com.android.providers.downloads.documents/tree/downloads => downloads

View File

@@ -19,7 +19,7 @@ class TrustedNetworkHelper(private val context: Context) {
var trustedNetworks: List<String>
get() {
val serializedNetworks = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_CUSTOM_TRUSTED_NETWORKS, "") ?: ""
return NETWORK_SSID_DELIMITER.split(serializedNetworks).filter { it.isNotEmpty() }
return serializedNetworks.split(NETWORK_SSID_DELIMITER, "#_#" /* TODO remove old delimiter in 2025 */).filter { it.isNotEmpty() }
}
set(value) {
PreferenceManager.getDefaultSharedPreferences(context)
@@ -36,7 +36,8 @@ class TrustedNetworkHelper(private val context: Context) {
.putBoolean(KEY_CUSTOM_TRUST_ALL_NETWORKS, value)
.apply()
val hasPermissions: Boolean = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
val hasPermissions: Boolean
get() = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
/** @return The current SSID or null if it's not available for any reason */
val currentSSID: String?
@@ -61,7 +62,7 @@ class TrustedNetworkHelper(private val context: Context) {
companion object {
private const val KEY_CUSTOM_TRUSTED_NETWORKS = "trusted_network_preference"
private const val KEY_CUSTOM_TRUST_ALL_NETWORKS = "trust_all_network_preference"
private const val NETWORK_SSID_DELIMITER = "#_#"
private const val NETWORK_SSID_DELIMITER = "\u0000"
private const val NOT_AVAILABLE_SSID_RESULT = "<unknown ssid>"
@JvmStatic

View File

@@ -22,6 +22,8 @@ import org.kde.kdeconnect.UserInterface.ThemeUtil
import org.kde.kdeconnect_tp.BuildConfig
import org.slf4j.impl.HandroidLoggerAdapter
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.Date
import java.util.concurrent.ConcurrentHashMap
/*
@@ -90,7 +92,7 @@ class KdeConnect : Application() {
}
private fun loadRememberedDevicesFromSettings() {
// Log.e("BackgroundService", "Loading remembered trusted devices");
// Log.e("BackgroundService", "Loading remembered trusted devices")
val preferences = getSharedPreferences("trusted_devices", MODE_PRIVATE)
val trustedDevices: Set<String> = preferences.all.keys
trustedDevices.map { id ->
@@ -99,6 +101,14 @@ class KdeConnect : Application() {
}.filter { preferences.getBoolean(it, false) }.forEach {
try {
val device = Device(applicationContext, it)
val now = Date()
val x509Cert = device.certificate as X509Certificate
if(now < x509Cert.notBefore) {
throw CertificateException("Certificate not effective yet: "+x509Cert.notBefore)
}
else if(now > x509Cert.notAfter) {
throw CertificateException("Certificate already expired: "+x509Cert.notAfter)
}
devices[it] = device
device.addPairingCallback(devicePairingCallback)
} catch (e: CertificateException) {
@@ -110,6 +120,16 @@ class KdeConnect : Application() {
}
}
}
fun removeRememberedDevices() {
// Log.e("BackgroundService", "Removing remembered trusted devices")
val preferences = getSharedPreferences("trusted_devices", MODE_PRIVATE)
val trustedDevices: Set<String> = preferences.all.keys
trustedDevices.filter { preferences.getBoolean(it, false) }
.forEach {
Log.d("KdeConnect", "Removing devices: $it")
preferences.edit().remove(it).apply()
}
}
private val devicePairingCallback: PairingCallback = object : PairingCallback {
override fun incomingPairRequest() {
@@ -158,7 +178,7 @@ class KdeConnect : Application() {
// - When a device becomes unreachable, if it's unpaired (this case here)
// - When a device becomes unpaired, if it's unreachable (not implemented ATM)
// if (!device.isReachable && !device.isPaired) {
// Log.i("onConnectionLost","Removing device because it was not paired");
// Log.i("onConnectionLost","Removing device because it was not paired")
// devices.remove(link.deviceId)
// device.removePairingCallback(devicePairingCallback)
// }

View File

@@ -1,64 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 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.Plugins.FindRemoteDevicePlugin;
import android.app.Activity;
import androidx.annotation.NonNull;
import org.apache.commons.lang3.ArrayUtils;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect_tp.R;
@PluginFactory.LoadablePlugin
public class FindRemoteDevicePlugin extends Plugin {
@Override
public @NonNull String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_findremotedevice);
}
@Override
public @NonNull String getDescription() {
return context.getResources().getString(R.string.pref_plugin_findremotedevice_desc);
}
@Override
public boolean onPacketReceived(@NonNull NetworkPacket np) {
return true;
}
@Override
public @NonNull String getActionName() {
return context.getString(R.string.ring);
}
@Override
public void startMainActivity(Activity activity) {
if (isDeviceInitialized()) {
getDevice().sendPacket(new NetworkPacket(FindMyPhonePlugin.PACKET_TYPE_FINDMYPHONE_REQUEST));
}
}
@Override
public boolean displayInContextMenu() {
return true;
}
@Override
public @NonNull String[] getSupportedPacketTypes() {
return ArrayUtils.EMPTY_STRING_ARRAY;
}
@Override
public @NonNull String[] getOutgoingPacketTypes() {
return new String[]{FindMyPhonePlugin.PACKET_TYPE_FINDMYPHONE_REQUEST};
}
}

View File

@@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2014 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.Plugins.FindRemoteDevicePlugin
import android.app.Activity
import org.kde.kdeconnect.NetworkPacket
import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin
import org.kde.kdeconnect.Plugins.Plugin
import org.kde.kdeconnect.Plugins.PluginFactory.LoadablePlugin
import org.kde.kdeconnect_tp.R
@LoadablePlugin
class FindRemoteDevicePlugin : Plugin() {
override val displayName: String
get() = context.resources.getString(R.string.pref_plugin_findremotedevice)
override val description: String
get() = context.resources.getString(R.string.pref_plugin_findremotedevice_desc)
override fun onPacketReceived(np: NetworkPacket): Boolean = true
override val actionName: String
get() = context.getString(R.string.ring)
override fun startMainActivity(activity: Activity) {
if (isDeviceInitialized) {
device.sendPacket(NetworkPacket(FindMyPhonePlugin.PACKET_TYPE_FINDMYPHONE_REQUEST))
}
}
override fun displayInContextMenu(): Boolean = true
override val supportedPacketTypes: Array<String>
get() = arrayOf()
override val outgoingPacketTypes: Array<String>
get() = arrayOf(FindMyPhonePlugin.PACKET_TYPE_FINDMYPHONE_REQUEST)
}

View File

@@ -13,6 +13,7 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
@@ -55,6 +56,7 @@ public class MousePadActivity
private double scrollCoefficient = 1.0;
private boolean allowGyro = false;
private boolean gyroEnabled = false;
private boolean doubleTapDragEnabled = false;
private int gyroscopeSensitivity = 100;
private boolean isScrolling = false;
private float accumulatedDistanceY = 0;
@@ -376,14 +378,16 @@ public class MousePadActivity
@Override
public void onLongPress(MotionEvent e) {
getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendSingleHold();
dragging = true;
if (!doubleTapDragEnabled) {
getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendSingleHold();
dragging = true;
}
}
@Override
@@ -415,13 +419,18 @@ public class MousePadActivity
finish();
return true;
}
plugin.sendDoubleClick();
if (doubleTapDragEnabled) {
plugin.sendSingleHold();
dragging = true;
} else {
plugin.sendDoubleClick();
}
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
return true;
}
@Override
@@ -578,6 +587,8 @@ public class MousePadActivity
findViewById(R.id.mouse_buttons).setVisibility(View.GONE);
}
doubleTapDragEnabled = prefs.getBoolean(getString(R.string.mousepad_doubletap_drag_enabled_pref), true);
prefsApplied = true;
}

View File

@@ -1,132 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 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.Plugins.PingPlugin;
import android.Manifest;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect_tp.R;
import java.util.Objects;
@PluginFactory.LoadablePlugin
public class PingPlugin extends Plugin {
private final static String PACKET_TYPE_PING = "kdeconnect.ping";
@Override
public @NonNull String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_ping);
}
@Override
public @NonNull String getDescription() {
return context.getResources().getString(R.string.pref_plugin_ping_desc);
}
@Override
public boolean onPacketReceived(@NonNull NetworkPacket np) {
if (!np.getType().equals(PACKET_TYPE_PING)) {
Log.e("PingPlugin", "Ping plugin should not receive packets other than pings!");
return false;
}
//Log.e("PingPacketReceiver", "was a ping!");
PendingIntent resultPendingIntent = PendingIntent.getActivity(
context,
0,
new Intent(context, MainActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
);
int id;
String message;
if (np.has("message")) {
message = np.getString("message");
id = (int) System.currentTimeMillis();
} else {
message = "Ping!";
id = 42; //A unique id to create only one notification
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
int permissionResult = ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS);
if (permissionResult != PackageManager.PERMISSION_GRANTED) {
// If notifications are not allowed, show a toast instead of a notification
new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
return true;
}
}
NotificationManager notificationManager = ContextCompat.getSystemService(context, NotificationManager.class);
Notification noti = new NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)
.setContentTitle(getDevice().getName())
.setContentText(message)
.setContentIntent(resultPendingIntent)
.setTicker(message)
.setSmallIcon(R.drawable.ic_notification)
.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_ALL)
.setStyle(new NotificationCompat.BigTextStyle().bigText(message))
.build();
NotificationHelper.notifyCompat(notificationManager, id, noti);
return true;
}
@Override
public @NonNull String getActionName() {
return context.getString(R.string.send_ping);
}
@Override
public void startMainActivity(Activity activity) {
if (isDeviceInitialized()) {
getDevice().sendPacket(new NetworkPacket(PACKET_TYPE_PING));
}
}
@Override
public boolean displayInContextMenu() {
return true;
}
@Override
public @NonNull String[] getSupportedPacketTypes() {
return new String[]{PACKET_TYPE_PING};
}
@Override
public @NonNull String[] getOutgoingPacketTypes() {
return new String[]{PACKET_TYPE_PING};
}
}

View File

@@ -0,0 +1,108 @@
/*
* SPDX-FileCopyrightText: 2014 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.Plugins.PingPlugin
import android.Manifest
import android.app.Activity
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.getSystemService
import org.kde.kdeconnect.Helpers.NotificationHelper
import org.kde.kdeconnect.NetworkPacket
import org.kde.kdeconnect.Plugins.Plugin
import org.kde.kdeconnect.Plugins.PluginFactory.LoadablePlugin
import org.kde.kdeconnect.UserInterface.MainActivity
import org.kde.kdeconnect_tp.R
@LoadablePlugin
class PingPlugin : Plugin() {
override val displayName: String
get() = context.resources.getString(R.string.pref_plugin_ping)
override val description: String
get() = context.resources.getString(R.string.pref_plugin_ping_desc)
override fun onPacketReceived(np: NetworkPacket): Boolean {
if (np.type != PACKET_TYPE_PING) {
Log.e(LOG_TAG, "Ping plugin should not receive packets other than pings!")
return false
}
val mutableUpdateFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
val resultPendingIntent = PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), mutableUpdateFlags)
val (id: Int, message: String) = if (np.has("message")) {
val id = System.currentTimeMillis().toInt()
Pair(id, np.getString("message"))
} else {
val id = 42 // A unique id to create only one notification
Pair(id, "Ping!")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permissionResult = ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
if (permissionResult != PackageManager.PERMISSION_GRANTED) {
// If notifications are not allowed, show a toast instead of a notification
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
return true
}
}
val notificationManager = context.getSystemService<NotificationManager>()
val notification = NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)
.setContentTitle(device.name)
.setContentText(message)
.setContentIntent(resultPendingIntent)
.setTicker(message)
.setSmallIcon(R.drawable.ic_notification)
.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_ALL)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.build()
NotificationHelper.notifyCompat(notificationManager, id, notification)
return true
}
override val actionName: String
get() = context.getString(R.string.send_ping)
override fun startMainActivity(parentActivity: Activity) {
if (isDeviceInitialized) {
device.sendPacket(NetworkPacket(PACKET_TYPE_PING))
}
}
override fun displayInContextMenu(): Boolean {
return true
}
override val supportedPacketTypes: Array<String>
get() = arrayOf(PACKET_TYPE_PING)
override val outgoingPacketTypes: Array<String>
get() = arrayOf(PACKET_TYPE_PING)
companion object {
private const val PACKET_TYPE_PING = "kdeconnect.ping"
private const val LOG_TAG = "PingPlugin"
}
}

View File

@@ -1,179 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 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.Plugins;
import static org.apache.commons.collections4.SetUtils.unmodifiableSet;
import android.content.Context;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.atteo.classindex.ClassIndex;
import org.atteo.classindex.IndexAnnotated;
import org.kde.kdeconnect.Device;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class PluginFactory {
public static void sortPluginList(@NonNull List<String> plugins) {
plugins.sort(Comparator.comparing(o -> pluginInfo.get(o).displayName));
}
@IndexAnnotated
public @interface LoadablePlugin { } //Annotate plugins with this so PluginFactory finds them
public static class PluginInfo {
PluginInfo(@NonNull String displayName, @NonNull String description, @DrawableRes int icon,
boolean enabledByDefault, boolean hasSettings, boolean supportsDeviceSpecificSettings,
boolean listenToUnpaired, @NonNull String[] supportedPacketTypes, @NonNull String[] outgoingPacketTypes,
@NonNull Class<? extends Plugin> instantiableClass) {
this.displayName = displayName;
this.description = description;
this.icon = icon;
this.enabledByDefault = enabledByDefault;
this.hasSettings = hasSettings;
this.supportsDeviceSpecificSettings = supportsDeviceSpecificSettings;
this.listenToUnpaired = listenToUnpaired;
this.supportedPacketTypes = unmodifiableSet(supportedPacketTypes);
this.outgoingPacketTypes = unmodifiableSet(outgoingPacketTypes);
this.instantiableClass = instantiableClass;
}
public @NonNull String getDisplayName() {
return displayName;
}
public @NonNull String getDescription() {
return description;
}
public @DrawableRes int getIcon() {
return icon;
}
public boolean hasSettings() {
return hasSettings;
}
public boolean supportsDeviceSpecificSettings() { return supportsDeviceSpecificSettings; }
public boolean isEnabledByDefault() {
return enabledByDefault;
}
public boolean listenToUnpaired() {
return listenToUnpaired;
}
Set<String> getOutgoingPacketTypes() {
return outgoingPacketTypes;
}
public Set<String> getSupportedPacketTypes() {
return supportedPacketTypes;
}
Class<? extends Plugin> getInstantiableClass() {
return instantiableClass;
}
private final @NonNull String displayName;
private final @NonNull String description;
private final @DrawableRes int icon;
private final boolean enabledByDefault;
private final boolean hasSettings;
private final boolean supportsDeviceSpecificSettings;
private final boolean listenToUnpaired;
private final @NonNull Set<String> supportedPacketTypes;
private final @NonNull Set<String> outgoingPacketTypes;
private final Class<? extends Plugin> instantiableClass;
}
private static final Map<String, PluginInfo> pluginInfo = new ConcurrentHashMap<>();
public static PluginInfo getPluginInfo(String pluginKey) {
return pluginInfo.get(pluginKey);
}
public static void initPluginInfo(Context context) {
try {
for (Class<?> pluginClass : ClassIndex.getAnnotated(LoadablePlugin.class)) {
Plugin p = ((Plugin) pluginClass.newInstance());
p.setContext(context, null);
PluginInfo info = new PluginInfo(p.getDisplayName(), p.getDescription(), p.getIcon(),
p.isEnabledByDefault(), p.hasSettings(), p.supportsDeviceSpecificSettings(),
p.listensToUnpairedDevices(), p.getSupportedPacketTypes(),
p.getOutgoingPacketTypes(), p.getClass());
pluginInfo.put(p.getPluginKey(), info);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
Log.i("PluginFactory","Loaded "+pluginInfo.size()+" plugins");
}
public static @NonNull Set<String> getAvailablePlugins() {
return pluginInfo.keySet();
}
public static @Nullable Plugin instantiatePluginForDevice(Context context, String pluginKey, Device device) {
PluginInfo info = pluginInfo.get(pluginKey);
try {
Plugin plugin = info.getInstantiableClass().newInstance();
plugin.setContext(context, device);
return plugin;
} catch (Exception e) {
Log.e("PluginFactory", "Could not instantiate plugin: " + pluginKey, e);
return null;
}
}
public static @NonNull Set<String> getIncomingCapabilities() {
HashSet<String> capabilities = new HashSet<>();
for (PluginInfo plugin : pluginInfo.values()) {
capabilities.addAll(plugin.getSupportedPacketTypes());
}
return capabilities;
}
public static @NonNull Set<String> getOutgoingCapabilities() {
HashSet<String> capabilities = new HashSet<>();
for (PluginInfo plugin : pluginInfo.values()) {
capabilities.addAll(plugin.getOutgoingPacketTypes());
}
return capabilities;
}
public static @NonNull Set<String> pluginsForCapabilities(Set<String> incoming, Set<String> outgoing) {
HashSet<String> plugins = new HashSet<>();
for (Map.Entry<String, PluginInfo> entry : pluginInfo.entrySet()) {
String pluginId = entry.getKey();
PluginInfo info = entry.getValue();
//Check incoming against outgoing
if (Collections.disjoint(outgoing, info.getSupportedPacketTypes())
&& Collections.disjoint(incoming, info.getOutgoingPacketTypes())) {
Log.d("PluginFactory", "Won't load " + pluginId + " because of unmatched capabilities");
continue; //No capabilities in common, do not load this plugin
}
plugins.add(pluginId);
}
return plugins;
}
}

View File

@@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2014 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.Plugins
import android.content.Context
import android.util.Log
import androidx.annotation.DrawableRes
import org.atteo.classindex.ClassIndex
import org.atteo.classindex.IndexAnnotated
import org.kde.kdeconnect.Device
object PluginFactory {
private var pluginInfo: Map<String, PluginInfo> = mapOf()
fun initPluginInfo(context: Context) {
try {
val plugins = ClassIndex.getAnnotated(LoadablePlugin::class.java)
.map { it.newInstance() as Plugin }
.map { plugin -> plugin.apply { setContext(context, null) } }
pluginInfo = plugins.associate { plugin -> Pair(plugin.pluginKey, PluginInfo(plugin)) }
} catch (e: Exception) {
throw RuntimeException(e)
}
Log.i("PluginFactory", "Loaded " + pluginInfo.size + " plugins")
}
val availablePlugins: Set<String>
get() = pluginInfo.keys
val incomingCapabilities: Set<String>
get() = pluginInfo.values.flatMap { plugin -> plugin.supportedPacketTypes }.toSet()
val outgoingCapabilities: Set<String>
get() = pluginInfo.values.flatMap { plugin -> plugin.outgoingPacketTypes }.toSet()
@JvmStatic
fun getPluginInfo(pluginKey: String): PluginInfo = pluginInfo[pluginKey]!!
@JvmStatic
fun sortPluginList(plugins: List<String>): List<String> {
return plugins.sortedBy { pluginInfo[it]?.displayName }
}
fun instantiatePluginForDevice(context: Context, pluginKey: String, device: Device): Plugin? {
try {
val plugin = pluginInfo[pluginKey]?.instantiableClass?.newInstance()?.apply { setContext(context, device) }
return plugin
} catch (e: Exception) {
Log.e("PluginFactory", "Could not instantiate plugin: $pluginKey", e)
return null
}
}
fun pluginsForCapabilities(incoming: Set<String>, outgoing: Set<String>): Set<String> {
fun hasCommonCapabilities(info: PluginInfo): Boolean =
outgoing.any { it in info.supportedPacketTypes } ||
incoming.any { it in info.outgoingPacketTypes }
val (used, unused) = pluginInfo.entries.partition { hasCommonCapabilities(it.value) }
for (pluginId in unused.map { it.key }) {
Log.d("PluginFactory", "Won't load $pluginId because of unmatched capabilities")
}
return used.map { it.key }.toSet()
}
@IndexAnnotated
annotation class LoadablePlugin //Annotate plugins with this so PluginFactory finds them
class PluginInfo private constructor(
val displayName: String,
val description: String,
// DrawableRes needs to be in the primary constructor, which is why we need 2 constructors
@field:DrawableRes @get:DrawableRes @param:DrawableRes val icon: Int,
val isEnabledByDefault: Boolean,
val hasSettings: Boolean,
val supportsDeviceSpecificSettings: Boolean,
val listenToUnpaired: Boolean,
supportedPacketTypes: Array<String>,
outgoingPacketTypes: Array<String>,
val instantiableClass: Class<out Plugin>,
) {
internal constructor(p: Plugin) : this(p.displayName, p.description, p.icon,
p.isEnabledByDefault, p.hasSettings(), p.supportsDeviceSpecificSettings(),
p.listensToUnpairedDevices(), p.supportedPacketTypes,
p.outgoingPacketTypes, p.javaClass)
val supportedPacketTypes: Set<String> = supportedPacketTypes.toSet()
val outgoingPacketTypes: Set<String> = outgoingPacketTypes.toSet()
}
}

View File

@@ -8,6 +8,7 @@
package org.kde.kdeconnect.Plugins.SMSPlugin;
import static androidx.core.content.ContextCompat.RECEIVER_EXPORTED;
import static org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin.PACKET_TYPE_TELEPHONY;
import android.Manifest;
@@ -375,7 +376,7 @@ public class SMSPlugin extends Plugin {
IntentFilter refreshFilter = new IntentFilter(Transaction.REFRESH);
refreshFilter.setPriority(500);
context.registerReceiver(messagesUpdateReceiver, refreshFilter);
context.registerReceiver(messagesUpdateReceiver, refreshFilter, RECEIVER_EXPORTED);
Looper helperLooper = SMSHelper.MessageLooper.getLooper();
ContentObserver messageObserver = new MessageContentObserver(new Handler(helperLooper));

View File

@@ -41,7 +41,7 @@ class SftpPlugin : Plugin(), OnSharedPreferenceChangeListener {
override fun onCreate(): Boolean = true
override fun checkRequiredPermissions(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return if (SimpleSftpServer.SUPPORTS_NATIVEFS) {
Environment.isExternalStorageManager()
} else {
SftpSettingsFragment.getStorageInfoList(context, this).size != 0
@@ -49,7 +49,7 @@ class SftpPlugin : Plugin(), OnSharedPreferenceChangeListener {
}
override val permissionExplanationDialog: AlertDialogFragment
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
get() = if (SimpleSftpServer.SUPPORTS_NATIVEFS) {
StartActivityAlertDialogFragment.Builder()
.setTitle(displayName)
.setMessage(R.string.sftp_manage_storage_permission_explanation)
@@ -90,7 +90,7 @@ class SftpPlugin : Plugin(), OnSharedPreferenceChangeListener {
val paths = mutableListOf<String>()
val pathNames = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (SimpleSftpServer.SUPPORTS_NATIVEFS) {
val volumes = context.getSystemService(
StorageManager::class.java
).storageVolumes
@@ -200,7 +200,7 @@ class SftpPlugin : Plugin(), OnSharedPreferenceChangeListener {
override val outgoingPacketTypes: Array<String> = arrayOf(PACKET_TYPE_SFTP)
override fun hasSettings(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
override fun hasSettings(): Boolean = !SimpleSftpServer.SUPPORTS_NATIVEFS
override fun supportsDeviceSpecificSettings(): Boolean = true

View File

@@ -86,7 +86,7 @@ internal class SimpleSftpServer {
fun initialize(context: Context, device: Device) {
val sshd = ServerBuilder.builder().apply {
fileSystemFactory(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (SUPPORTS_NATIVEFS) {
NativeFileSystemFactory()
} else {
safFileSystemFactory = SafFileSystemFactory(context)
@@ -262,6 +262,8 @@ internal class SimpleSftpServer {
companion object {
private const val TAG = "SimpleSftpServer"
val SUPPORTS_NATIVEFS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
private val PORT_RANGE = 1739..1764
const val USER: String = "kdeconnect"

View File

@@ -88,7 +88,7 @@ public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
}
private Device getDevice() {
return requestInfo;
return getRequestInfo();
}
boolean isRunning() { return isRunning; }
@@ -131,7 +131,7 @@ public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
isRunning = true;
while (!done && !canceled) {
while (!done && !isCancelled()) {
synchronized (lock) {
currentNetworkPacket = networkPacketList.get(0);
}
@@ -153,7 +153,7 @@ public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
if ( received != currentNetworkPacket.getPayloadSize()) {
fileDocument.delete();
if (!canceled) {
if (!isCancelled()) {
throw new RuntimeException("Failed to receive: " + currentFileName + " received:" + received + " bytes, expected: " + currentNetworkPacket.getPayloadSize() + " bytes");
}
} else {
@@ -184,7 +184,7 @@ public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
listIsEmpty = networkPacketList.isEmpty();
}
if (listIsEmpty && !canceled) {
if (listIsEmpty && !isCancelled()) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
@@ -203,7 +203,7 @@ public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
isRunning = false;
if (canceled) {
if (isCancelled()) {
receiveNotification.cancel();
return;
}
@@ -290,7 +290,7 @@ public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
int count;
long received = 0;
while ((count = input.read(data)) >= 0 && !canceled) {
while ((count = input.read(data)) >= 0 && !isCancelled()) {
received += count;
totalReceived += count;

View File

@@ -79,7 +79,7 @@ public class CompositeUploadFileJob extends BackgroundJob<Device, Void> {
sendPacketStatusCallback = new SendPacketStatusCallback();
}
private Device getDevice() { return requestInfo; }
private Device getDevice() { return getRequestInfo(); }
@Override
public void run() {
@@ -92,7 +92,7 @@ public class CompositeUploadFileJob extends BackgroundJob<Device, Void> {
}
try {
while (!done && !canceled) {
while (!done && !isCancelled()) {
synchronized (lock) {
currentNetworkPacket = networkPacketList.remove(0);
}
@@ -115,7 +115,7 @@ public class CompositeUploadFileJob extends BackgroundJob<Device, Void> {
}
}
if (canceled) {
if (isCancelled()) {
uploadNotification.cancel();
} else {
uploadNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.sent_files_title, currentFileNum, getDevice().getName(), currentFileNum));

View File

@@ -81,7 +81,7 @@ public class SharePlugin extends Plugin {
private SharedPreferences mSharedPrefs;
public SharePlugin() {
backgroundJobHandler = BackgroundJobHandler.newFixedThreadPoolBackgroundJobHander(5);
backgroundJobHandler = BackgroundJobHandler.newFixedThreadPoolBackgroundJobHandler(5);
handler = new Handler(Looper.getMainLooper());
receiveFileJobCallback = new Callback();
}

View File

@@ -38,7 +38,7 @@ public class PluginPreference extends SwitchPreference {
setSummary(info.getDescription());
setChecked(device.isPluginEnabled(pluginKey));
if (info.hasSettings()) {
if (info.getHasSettings()) {
this.listener = v -> {
Plugin plugin = device.getPluginIncludingWithoutPermissions(pluginKey);
if (plugin != null) {

View File

@@ -83,9 +83,8 @@ public class PluginSettingsListFragment extends PreferenceFragmentCompat {
activity.runOnUiThread(activity::finish);
return;
}
List<String> plugins = device.getSupportedPlugins();
PluginFactory.sortPluginList(plugins);
List<String> plugins = PluginFactory.sortPluginList(device.getSupportedPlugins());
for (final String pluginKey : plugins) {
//TODO: Use PreferenceManagers context
PluginPreference pref = new PluginPreference(requireContext(), pluginKey, device, callback);

View File

@@ -1,62 +0,0 @@
/*
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.async;
import androidx.annotation.NonNull;
import java.util.concurrent.atomic.AtomicLong;
public abstract class BackgroundJob<I, R> implements Runnable {
private static final AtomicLong atomicLong = new AtomicLong(0);
protected volatile boolean canceled;
private BackgroundJobHandler backgroundJobHandler;
private final long id;
protected final I requestInfo;
private final Callback<R> callback;
public BackgroundJob(I requestInfo, Callback<R> callback) {
this.id = atomicLong.incrementAndGet();
this.requestInfo = requestInfo;
this.callback = callback;
}
void setBackgroundJobHandler(BackgroundJobHandler handler) {
this.backgroundJobHandler = handler;
}
public long getId() { return id; }
public I getRequestInfo() { return requestInfo; }
public void cancel() {
canceled = true;
backgroundJobHandler.cancelJob(this);
}
public boolean isCancelled() {
return canceled;
}
public interface Callback<R> {
void onResult(@NonNull BackgroundJob job, R result);
void onError(@NonNull BackgroundJob job, @NonNull Throwable error);
}
protected void reportResult(R result) {
backgroundJobHandler.runOnUiThread(() -> {
callback.onResult(this, result);
backgroundJobHandler.onFinished(this);
});
}
protected void reportError(@NonNull Throwable error) {
backgroundJobHandler.runOnUiThread(() -> {
callback.onError(this, error);
backgroundJobHandler.onFinished(this);
});
}
}

View File

@@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.async
import java.util.concurrent.atomic.AtomicLong
import kotlin.concurrent.Volatile
abstract class BackgroundJob<I, R> : Runnable {
private val callback: Callback<R>
val requestInfo: I
val id: Long
constructor(requestInfo: I, callback: Callback<R>) {
this.callback = callback
this.requestInfo = requestInfo
this.id = idIncrementer.incrementAndGet()
}
@Volatile
var isCancelled: Boolean = false
protected set
private var backgroundJobHandler: BackgroundJobHandler? = null
/** Used by the job handler to register itself as the handler */
fun setBackgroundJobHandler(handler: BackgroundJobHandler) {
this.backgroundJobHandler = handler
}
open fun cancel() {
isCancelled = true
backgroundJobHandler!!.cancelJob(this)
}
interface Callback<R> {
fun onResult(job: BackgroundJob<*, *>, result: R)
fun onError(job: BackgroundJob<*, *>, error: Throwable)
}
protected fun reportResult(result: R) {
backgroundJobHandler!!.runOnUiThread {
callback.onResult(this, result)
backgroundJobHandler!!.onFinished(this)
}
}
fun reportError(error: Throwable) {
backgroundJobHandler!!.runOnUiThread {
callback.onError(this, error)
backgroundJobHandler!!.onFinished(this)
}
}
companion object {
private val idIncrementer = AtomicLong(0)
}
}

View File

@@ -1,163 +0,0 @@
/*
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.async;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Scheduler for {@link BackgroundJob} objects.
* <p>
* We use an internal {@link ThreadPoolExecutor} to catch Exceptions and
* pass them along to {@link #handleUncaughtException(Future, Throwable)}.
* </p>
*/
public class BackgroundJobHandler {
private static final String TAG = BackgroundJobHandler.class.getSimpleName();
private final Map<BackgroundJob, Future<?>> jobMap = new HashMap<>();
private final Object jobMapLock = new Object();
private class MyThreadPoolExecutor extends ThreadPoolExecutor {
MyThreadPoolExecutor(int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (!(r instanceof Future)) {
return;
}
Future<?> future = (Future<?>) r;
if (t == null) {
try {
future.get();
} catch (CancellationException ce) {
Log.d(TAG,"afterExecute got a CancellationException");
} catch (ExecutionException ee) {
t = ee;
} catch (InterruptedException ie) {
Log.d(TAG, "afterExecute got an InterruptedException");
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null) {
BackgroundJobHandler.this.handleUncaughtException(future, t);
}
}
}
private final ThreadPoolExecutor threadPoolExecutor;
private final Handler handler;
private BackgroundJobHandler(int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this.handler = new Handler(Looper.getMainLooper());
this.threadPoolExecutor = new MyThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue);
}
public void runJob(BackgroundJob bgJob) {
Future<?> f;
bgJob.setBackgroundJobHandler(this);
try {
synchronized (jobMapLock) {
f = threadPoolExecutor.submit(bgJob);
jobMap.put(bgJob, f);
}
} catch (RejectedExecutionException e) {
Log.d(TAG,"threadPoolExecutor.submit rejected a background job: " + e.getMessage());
bgJob.reportError(e);
}
}
public boolean isRunning(long jobId) {
synchronized (jobMapLock) {
for (BackgroundJob job : jobMap.keySet()) {
if (job.getId() == jobId) {
return true;
}
}
}
return false;
}
@Nullable
public BackgroundJob getJob(long jobId) {
synchronized (jobMapLock) {
for (BackgroundJob job : jobMap.keySet()) {
if (job.getId() == jobId) {
return job;
}
}
}
return null;
}
void cancelJob(BackgroundJob job) {
synchronized (jobMapLock) {
if (jobMap.containsKey(job)) {
Future<?> f = jobMap.get(job);
if (f.cancel(true)) {
threadPoolExecutor.purge();
}
jobMap.remove(job);
}
}
}
private void handleUncaughtException(Future<?> ft, Throwable t) {
synchronized (jobMapLock) {
for (Map.Entry<BackgroundJob, Future<?>> pairs : jobMap.entrySet()) {
Future<?> future = pairs.getValue();
if (future == ft) {
pairs.getKey().reportError(t);
break;
}
}
}
}
void onFinished(BackgroundJob job) {
synchronized (jobMapLock) {
jobMap.remove(job);
}
}
void runOnUiThread(Runnable runnable) {
handler.post(runnable);
}
public static BackgroundJobHandler newFixedThreadPoolBackgroundJobHander(int numThreads) {
return new BackgroundJobHandler(numThreads, numThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
}
}

View File

@@ -0,0 +1,133 @@
/*
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.async
import android.os.Handler
import android.os.Looper
import android.util.Log
import java.util.concurrent.BlockingQueue
import java.util.concurrent.CancellationException
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
/**
* Scheduler for [BackgroundJob] objects.
*
* We use an internal [ThreadPoolExecutor] to catch Exceptions and pass them along to [.handleUncaughtException].
*
* Might be able to be replaced with coroutines later
*/
class BackgroundJobHandler {
private constructor(corePoolSize: Int, maxPoolSize: Int, keepAliveTime: Long, unit: TimeUnit, workQueue: BlockingQueue<Runnable>) {
this.threadPoolExecutor = MyThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue)
this.handler = Handler(Looper.getMainLooper())
}
private val jobMap: MutableMap<BackgroundJob<*, *>, Future<*>> = HashMap()
private val jobMapLock: Any = Any()
private inner class MyThreadPoolExecutor(corePoolSize: Int, maxPoolSize: Int, keepAliveTime: Long, unit: TimeUnit, workQueue: BlockingQueue<Runnable>) : ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue) {
override fun afterExecute(runnable: Runnable, throwable: Throwable?) {
super.afterExecute(runnable, throwable)
if (runnable !is Future<*>) {
return
}
val future = runnable as Future<*>
var t: Throwable? = throwable
if (t == null) {
try {
future.get()
}
catch (ce: CancellationException) {
Log.d(LOG_TAG, "afterExecute got a CancellationException")
}
catch (ee: ExecutionException) {
t = ee
}
catch (ie: InterruptedException) {
Log.d(LOG_TAG, "afterExecute got an InterruptedException")
Thread.currentThread().interrupt() // ignore / reset
}
}
if (t != null) {
this@BackgroundJobHandler.handleUncaughtException(future, t)
}
}
}
private val threadPoolExecutor: ThreadPoolExecutor
private val handler: Handler
fun runJob(bgJob: BackgroundJob<*, *>) {
bgJob.setBackgroundJobHandler(this)
try {
synchronized(jobMapLock) {
val future: Future<*> = threadPoolExecutor.submit(bgJob)
jobMap.put(bgJob, future)
}
}
catch (e: RejectedExecutionException) {
Log.d(LOG_TAG, "threadPoolExecutor.submit rejected a background job: ${e.message}")
bgJob.reportError(e)
}
}
fun isRunning(jobId: Long): Boolean {
synchronized(jobMapLock) {
return jobMap.keys.any { it.id == jobId }
}
}
fun getJob(jobId: Long): BackgroundJob<*, *>? {
synchronized(jobMapLock) {
return jobMap.keys.find { it.id == jobId }
}
}
fun cancelJob(job: BackgroundJob<*, *>) {
synchronized(jobMapLock) {
if (jobMap.containsKey(job)) {
if (jobMap[job]!!.cancel(true)) {
threadPoolExecutor.purge()
}
jobMap.remove(job)
}
}
}
private fun handleUncaughtException(ft: Future<*>, t: Throwable) {
synchronized(jobMapLock) {
jobMap.entries.find { it.value == ft }?.key?.reportError(t)
}
}
fun onFinished(job: BackgroundJob<*, *>) {
synchronized(jobMapLock) {
jobMap.remove(job)
}
}
fun runOnUiThread(runnable: Runnable) {
handler.post(runnable)
}
companion object {
private val LOG_TAG: String = BackgroundJobHandler::class.java.simpleName
@JvmStatic
fun newFixedThreadPoolBackgroundJobHandler(numThreads: Int): BackgroundJobHandler {
return BackgroundJobHandler(numThreads, numThreads, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue())
}
}
}

View File

@@ -1,253 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015 Vineet Garg <grg.vineet@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
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.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Base64;
import androidx.core.content.ContextCompat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.kde.kdeconnect.Backends.LanBackend.LanLink;
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.mockito.MockedStatic;
import org.mockito.Mockito;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.HashSet;
public class DeviceTest {
private Context context;
MockedStatic<Base64> mockBase64;
MockedStatic<PreferenceManager> preferenceManager;
MockedStatic<ContextCompat> contextCompat;
@After
public void tearDown() {
mockBase64.close();
preferenceManager.close();
contextCompat.close();
}
// Creating a paired device before each test case
@Before
public void setUp() {
// Save new test device in settings
String deviceId = "testDevice";
String name = "Test Device";
String encodedCertificate = "MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2\n" +
"NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u\n" +
"bmVjdDAeFw0xNTA2MDMxMzE0MzhaFw0yNTA2MDMxMzE0MzhaMFUxLzAtBgNVBAMMJl9kYTE3OWE5\n" +
"MV9mMDY0XzQ3OGVfYmU4Y18xOTM1ZDc1NDRkNTRfMQwwCgYDVQQKDANLREUxFDASBgNVBAsMC0tk\n" +
"ZSBjb25uZWN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzH9GxS1lctpwYdSGAoPH\n" +
"ws+MnVaL0PVDCuzrpxzXc+bChR87xofhQIesLPLZEcmUJ1MlEJ6jx4W+gVhvY2tUN7SoiKKbnq8s\n" +
"WjI5ovs5yML3C1zPbOSJAdK613FcdkK+UGd/9dQk54gIozinC58iyTAChVVpB3pAF38EPxwKkuo2\n" +
"qTzwk24d6PRxz1skkzwEphUQQzGboyHsAlJHN1MzM2/yFGB4l8iUua2d3ETyfy/xFEh/SwtGtXE5\n" +
"KLz4cpb0fxjeYQZVruBKxzE07kgDO3zOhmP3LJ/KSPHWYImd1DWmpY9iDvoXr6+V7FAnRloaEIyg\n" +
"7WwdlSCpo3TXVuIjLwIDAQABozIwMDAdBgNVHQ4EFgQUwmbHo8YbiR463GRKSLL3eIKyvDkwDwYD\n" +
"VR0TAQH/BAUwAwIBADANBgkqhkiG9w0BAQUFAAOCAQEAydijH3rbnvpBDB/30w2PCGMT7O0N/XYM\n" +
"wBtUidqa4NFumJrNrccx5Ehp4UP66BfP61HW8h2U/EekYfOsZyyWd4KnsDD6ycR8h/WvpK3BC2cn\n" +
"I299wbqCEZmk5ZFFaEIDHdLAdgMCuxJkAzy9mMrWEa05Soxi2/ZXdrU9nXo5dzuPGYlirVPDHl7r\n" +
"/urBxD6HVX3ObQJRJ7r/nAWyUVdX3/biJaDRsydftOpGU6Gi5c1JK4MWIz8Bsjh6mEjCsVatbPPl\n" +
"yygGiJbDZfAvN2XoaVEBii2GDDCWfaFwPVPYlNTvjkUkMP8YThlMsiJ8Q4693XoLOL94GpNlCfUg\n" +
"7n+KOQ==";
this.context = Mockito.mock(Context.class);
mockBase64 = Mockito.mockStatic(Base64.class);
mockBase64.when(() -> Base64.encodeToString(any(byte[].class), anyInt())).thenAnswer(invocation -> java.util.Base64.getMimeEncoder().encodeToString((byte[]) invocation.getArguments()[0]));
mockBase64.when(() -> Base64.decode(anyString(), anyInt())).thenAnswer(invocation -> java.util.Base64.getMimeDecoder().decode((String) invocation.getArguments()[0]));
//Store device information needed to create a Device object in a future
MockSharedPreference deviceSettings = new MockSharedPreference();
SharedPreferences.Editor editor = deviceSettings.edit();
editor.putString("deviceName", name);
editor.putString("deviceType", DeviceType.PHONE.toString());
editor.putString("certificate", encodedCertificate);
editor.apply();
Mockito.when(context.getSharedPreferences(eq(deviceId), eq(Context.MODE_PRIVATE))).thenReturn(deviceSettings);
//Store the device as trusted
MockSharedPreference trustedSettings = new MockSharedPreference();
trustedSettings.edit().putBoolean(deviceId, true).apply();
Mockito.when(context.getSharedPreferences(eq("trusted_devices"), eq(Context.MODE_PRIVATE))).thenReturn(trustedSettings);
//Store an untrusted device
MockSharedPreference untrustedSettings = new MockSharedPreference();
Mockito.when(context.getSharedPreferences(eq("unpairedTestDevice"), eq(Context.MODE_PRIVATE))).thenReturn(untrustedSettings);
//Default shared prefs, including our own private key
preferenceManager = Mockito.mockStatic(PreferenceManager.class);
MockSharedPreference defaultSettings = new MockSharedPreference();
preferenceManager.when(() -> PreferenceManager.getDefaultSharedPreferences(any(Context.class))).thenReturn(defaultSettings);
RsaHelper.initialiseRsaKeys(context);
contextCompat = Mockito.mockStatic(ContextCompat.class);
contextCompat.when(() -> ContextCompat.getSystemService(context, NotificationManager.class)).thenReturn(Mockito.mock(NotificationManager.class));
}
@Test
public void testDeviceInfoToIdentityPacket() throws CertificateException {
String deviceId = "testDevice";
SharedPreferences settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
DeviceInfo di = DeviceInfo.loadFromSettings(context, deviceId, settings);
di.protocolVersion = DeviceHelper.ProtocolVersion;
di.incomingCapabilities = new HashSet<>(Arrays.asList("kdeconnect.plugin1State", "kdeconnect.plugin2State"));
di.outgoingCapabilities = new HashSet<>(Arrays.asList("kdeconnect.plugin1State.request", "kdeconnect.plugin2State.request"));
NetworkPacket np = di.toIdentityPacket();
assertEquals(di.id, np.getString("deviceId"));
assertEquals(di.name, np.getString("deviceName"));
assertEquals(di.protocolVersion, np.getInt("protocolVersion"));
assertEquals(di.type.toString(), np.getString("deviceType"));
assertEquals(di.incomingCapabilities, np.getStringSet("incomingCapabilities"));
assertEquals(di.outgoingCapabilities, np.getStringSet("outgoingCapabilities"));
}
@Test
public void testIsValidIdentityPacket() {
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY);
assertFalse(DeviceInfo.Companion.isValidIdentityPacket(np));
String validName = "MyDevice";
String validId = "123";
np.set("deviceName", validName);
np.set("deviceId", validId);
assertTrue(DeviceInfo.Companion.isValidIdentityPacket(np));
np.set("deviceName", " ");
assertFalse(DeviceInfo.Companion.isValidIdentityPacket(np));
np.set("deviceName", "<><><><><><><><><>"); // Only invalid characters
assertFalse(DeviceInfo.Companion.isValidIdentityPacket(np));
np.set("deviceName", validName);
np.set("deviceId", " ");
assertFalse(DeviceInfo.Companion.isValidIdentityPacket(np));
}
@Test
public void testDeviceType() {
assertEquals(DeviceType.PHONE, DeviceType.fromString(DeviceType.PHONE.toString()));
assertEquals(DeviceType.TABLET, DeviceType.fromString(DeviceType.TABLET.toString()));
assertEquals(DeviceType.DESKTOP, DeviceType.fromString(DeviceType.DESKTOP.toString()));
assertEquals(DeviceType.LAPTOP, DeviceType.fromString(DeviceType.LAPTOP.toString()));
assertEquals(DeviceType.TV, DeviceType.fromString(DeviceType.TV.toString()));
assertEquals(DeviceType.DESKTOP, DeviceType.fromString("invalid"));
}
// Basic paired device testing
@Test
public void testDevice() throws CertificateException {
Device device = new Device(context, "testDevice");
assertEquals(device.getDeviceId(), "testDevice");
assertEquals(device.getDeviceType(), DeviceType.PHONE);
assertEquals(device.getName(), "Test Device");
assertTrue(device.isPaired());
assertNotNull(device.getDeviceInfo().certificate);
}
@Test
public void testPairingDone() throws CertificateException {
NetworkPacket fakeNetworkPacket = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY);
String deviceId = "unpairedTestDevice";
fakeNetworkPacket.set("deviceId", deviceId);
fakeNetworkPacket.set("deviceName", "Unpaired Test Device");
fakeNetworkPacket.set("protocolVersion", DeviceHelper.ProtocolVersion);
fakeNetworkPacket.set("deviceType", DeviceType.PHONE.toString());
String certificateString =
"MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2\n" +
"NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u\n" +
"bmVjdDAeFw0xNTA2MDMxMzE0MzhaFw0yNTA2MDMxMzE0MzhaMFUxLzAtBgNVBAMMJl9kYTE3OWE5\n" +
"MV9mMDY0XzQ3OGVfYmU4Y18xOTM1ZDc1NDRkNTRfMQwwCgYDVQQKDANLREUxFDASBgNVBAsMC0tk\n" +
"ZSBjb25uZWN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzH9GxS1lctpwYdSGAoPH\n" +
"ws+MnVaL0PVDCuzrpxzXc+bChR87xofhQIesLPLZEcmUJ1MlEJ6jx4W+gVhvY2tUN7SoiKKbnq8s\n" +
"WjI5ovs5yML3C1zPbOSJAdK613FcdkK+UGd/9dQk54gIozinC58iyTAChVVpB3pAF38EPxwKkuo2\n" +
"qTzwk24d6PRxz1skkzwEphUQQzGboyHsAlJHN1MzM2/yFGB4l8iUua2d3ETyfy/xFEh/SwtGtXE5\n" +
"KLz4cpb0fxjeYQZVruBKxzE07kgDO3zOhmP3LJ/KSPHWYImd1DWmpY9iDvoXr6+V7FAnRloaEIyg\n" +
"7WwdlSCpo3TXVuIjLwIDAQABozIwMDAdBgNVHQ4EFgQUwmbHo8YbiR463GRKSLL3eIKyvDkwDwYD\n" +
"VR0TAQH/BAUwAwIBADANBgkqhkiG9w0BAQUFAAOCAQEAydijH3rbnvpBDB/30w2PCGMT7O0N/XYM\n" +
"wBtUidqa4NFumJrNrccx5Ehp4UP66BfP61HW8h2U/EekYfOsZyyWd4KnsDD6ycR8h/WvpK3BC2cn\n" +
"I299wbqCEZmk5ZFFaEIDHdLAdgMCuxJkAzy9mMrWEa05Soxi2/ZXdrU9nXo5dzuPGYlirVPDHl7r\n" +
"/urBxD6HVX3ObQJRJ7r/nAWyUVdX3/biJaDRsydftOpGU6Gi5c1JK4MWIz8Bsjh6mEjCsVatbPPl\n" +
"yygGiJbDZfAvN2XoaVEBii2GDDCWfaFwPVPYlNTvjkUkMP8YThlMsiJ8Q4693XoLOL94GpNlCfUg\n" +
"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);
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(), DeviceType.PHONE);
assertNotNull(device.getDeviceInfo().certificate);
device.getPairingHandler().pairingDone();
assertTrue(device.isPaired());
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
assertTrue(preferences.getBoolean(device.getDeviceId(), false));
SharedPreferences settings = context.getSharedPreferences(device.getDeviceId(), Context.MODE_PRIVATE);
assertEquals(settings.getString("deviceName", "Unknown device"), "Unpaired Test Device");
assertEquals(settings.getString("deviceType", "tablet"), "phone");
// Cleanup for unpaired test device
preferences.edit().remove(device.getDeviceId()).apply();
settings.edit().clear().apply();
}
@Test
public void testUnpair() throws CertificateException {
PairingHandler.PairingCallback pairingCallback = Mockito.mock(PairingHandler.PairingCallback.class);
Device device = new Device(context, "testDevice");
device.addPairingCallback(pairingCallback);
device.unpair();
assertFalse(device.isPaired());
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
assertFalse(preferences.getBoolean(device.getDeviceId(), false));
Mockito.verify(pairingCallback, Mockito.times(1)).unpaired();
}
}

View File

@@ -0,0 +1,269 @@
/*
* SPDX-FileCopyrightText: 2015 Vineet Garg <grg.vineet@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.app.NotificationManager
import android.content.Context
import android.preference.PreferenceManager
import android.util.Base64
import androidx.core.content.ContextCompat
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.kde.kdeconnect.Backends.LanBackend.LanLink
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider
import org.kde.kdeconnect.DeviceInfo.Companion.fromIdentityPacketAndCert
import org.kde.kdeconnect.DeviceInfo.Companion.isValidIdentityPacket
import org.kde.kdeconnect.DeviceInfo.Companion.loadFromSettings
import org.kde.kdeconnect.DeviceType.Companion.fromString
import org.kde.kdeconnect.Helpers.DeviceHelper
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
import org.kde.kdeconnect.PairingHandler.PairingCallback
import org.mockito.ArgumentMatchers
import org.mockito.MockedStatic
import org.mockito.Mockito
import org.mockito.invocation.InvocationOnMock
import java.security.cert.CertificateException
class DeviceTest {
private lateinit var context: Context
private lateinit var mockBase64: MockedStatic<Base64>
private lateinit var preferenceManager: MockedStatic<PreferenceManager>
private lateinit var contextCompat: MockedStatic<ContextCompat>
// Creating a paired device before each test case
@Before
fun setUp() {
// Save new test device in settings
val deviceId = "testDevice"
val name = "Test Device"
val encodedCertificate = """
MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2
NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u
bmVjdDAeFw0xNTA2MDMxMzE0MzhaFw0yNTA2MDMxMzE0MzhaMFUxLzAtBgNVBAMMJl9kYTE3OWE5
MV9mMDY0XzQ3OGVfYmU4Y18xOTM1ZDc1NDRkNTRfMQwwCgYDVQQKDANLREUxFDASBgNVBAsMC0tk
ZSBjb25uZWN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzH9GxS1lctpwYdSGAoPH
ws+MnVaL0PVDCuzrpxzXc+bChR87xofhQIesLPLZEcmUJ1MlEJ6jx4W+gVhvY2tUN7SoiKKbnq8s
WjI5ovs5yML3C1zPbOSJAdK613FcdkK+UGd/9dQk54gIozinC58iyTAChVVpB3pAF38EPxwKkuo2
qTzwk24d6PRxz1skkzwEphUQQzGboyHsAlJHN1MzM2/yFGB4l8iUua2d3ETyfy/xFEh/SwtGtXE5
KLz4cpb0fxjeYQZVruBKxzE07kgDO3zOhmP3LJ/KSPHWYImd1DWmpY9iDvoXr6+V7FAnRloaEIyg
7WwdlSCpo3TXVuIjLwIDAQABozIwMDAdBgNVHQ4EFgQUwmbHo8YbiR463GRKSLL3eIKyvDkwDwYD
VR0TAQH/BAUwAwIBADANBgkqhkiG9w0BAQUFAAOCAQEAydijH3rbnvpBDB/30w2PCGMT7O0N/XYM
wBtUidqa4NFumJrNrccx5Ehp4UP66BfP61HW8h2U/EekYfOsZyyWd4KnsDD6ycR8h/WvpK3BC2cn
I299wbqCEZmk5ZFFaEIDHdLAdgMCuxJkAzy9mMrWEa05Soxi2/ZXdrU9nXo5dzuPGYlirVPDHl7r
/urBxD6HVX3ObQJRJ7r/nAWyUVdX3/biJaDRsydftOpGU6Gi5c1JK4MWIz8Bsjh6mEjCsVatbPPl
yygGiJbDZfAvN2XoaVEBii2GDDCWfaFwPVPYlNTvjkUkMP8YThlMsiJ8Q4693XoLOL94GpNlCfUg
7n+KOQ==
""".trimIndent()
val context = Mockito.mock(Context::class.java)
val mockBase64 = Mockito.mockStatic(Base64::class.java)
mockBase64.`when`<Any> {
Base64.encodeToString(ArgumentMatchers.any(ByteArray::class.java), ArgumentMatchers.anyInt())
}.thenAnswer { invocation: InvocationOnMock ->
java.util.Base64.getMimeEncoder().encodeToString(invocation.arguments[0] as ByteArray)
}
mockBase64.`when`<Any> {
Base64.decode(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())
}.thenAnswer { invocation: InvocationOnMock ->
java.util.Base64.getMimeDecoder().decode(invocation.arguments[0] as String)
}
// Store device information needed to create a Device object in a future
val deviceSettings = MockSharedPreference()
val editor = deviceSettings.edit()
editor.putString("deviceName", name)
editor.putString("deviceType", DeviceType.PHONE.toString())
editor.putString("certificate", encodedCertificate)
editor.apply()
Mockito.`when`(context.getSharedPreferences(ArgumentMatchers.eq(deviceId), ArgumentMatchers.eq(Context.MODE_PRIVATE))).thenReturn(deviceSettings)
// Store the device as trusted
val trustedSettings = MockSharedPreference()
trustedSettings.edit().putBoolean(deviceId, true).apply()
Mockito.`when`(context.getSharedPreferences(ArgumentMatchers.eq("trusted_devices"), ArgumentMatchers.eq(Context.MODE_PRIVATE))).thenReturn(trustedSettings)
// Store an untrusted device
val untrustedSettings = MockSharedPreference()
Mockito.`when`(context.getSharedPreferences(ArgumentMatchers.eq("unpairedTestDevice"), ArgumentMatchers.eq(Context.MODE_PRIVATE))).thenReturn(untrustedSettings)
// Default shared prefs, including our own private key
val preferenceManager = Mockito.mockStatic(PreferenceManager::class.java)
val defaultSettings = MockSharedPreference()
preferenceManager.`when`<Any> {
PreferenceManager.getDefaultSharedPreferences(ArgumentMatchers.any(Context::class.java))
}.thenReturn(defaultSettings)
RsaHelper.initialiseRsaKeys(context)
val contextCompat = Mockito.mockStatic(ContextCompat::class.java)
contextCompat.`when`<Any> {
ContextCompat.getSystemService(context!!, NotificationManager::class.java)
}.thenReturn(Mockito.mock(NotificationManager::class.java))
this.context = context
this.mockBase64 = mockBase64
this.preferenceManager = preferenceManager
this.contextCompat = contextCompat
}
@After
fun tearDown() {
mockBase64.close()
preferenceManager.close()
contextCompat.close()
}
@Test
@Throws(CertificateException::class)
fun testDeviceInfoToIdentityPacket() {
val deviceId = "testDevice"
val settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE)
val deviceInfo = loadFromSettings(context, deviceId, settings)
deviceInfo.protocolVersion = DeviceHelper.ProtocolVersion
deviceInfo.incomingCapabilities = hashSetOf("kdeconnect.plugin1State", "kdeconnect.plugin2State")
deviceInfo.outgoingCapabilities = hashSetOf("kdeconnect.plugin1State.request", "kdeconnect.plugin2State.request")
val networkPacket = deviceInfo.toIdentityPacket()
Assert.assertEquals(deviceInfo.id, networkPacket.getString("deviceId"))
Assert.assertEquals(deviceInfo.name, networkPacket.getString("deviceName"))
Assert.assertEquals(deviceInfo.protocolVersion.toLong(), networkPacket.getInt("protocolVersion").toLong())
Assert.assertEquals(deviceInfo.type.toString(), networkPacket.getString("deviceType"))
Assert.assertEquals(deviceInfo.incomingCapabilities, networkPacket.getStringSet("incomingCapabilities"))
Assert.assertEquals(deviceInfo.outgoingCapabilities, networkPacket.getStringSet("outgoingCapabilities"))
}
@Test
fun testIsValidIdentityPacket() {
val np = NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY)
Assert.assertFalse(isValidIdentityPacket(np))
val validName = "MyDevice"
val validId = "123"
np["deviceName"] = validName
np["deviceId"] = validId
Assert.assertTrue(isValidIdentityPacket(np))
np["deviceName"] = " "
Assert.assertFalse(isValidIdentityPacket(np))
np["deviceName"] = "<><><><><><><><><>" // Only invalid characters
Assert.assertFalse(isValidIdentityPacket(np))
np["deviceName"] = validName
np["deviceId"] = " "
Assert.assertFalse(isValidIdentityPacket(np))
}
@Test
fun testDeviceType() {
Assert.assertEquals(DeviceType.PHONE, fromString(DeviceType.PHONE.toString()))
Assert.assertEquals(DeviceType.TABLET, fromString(DeviceType.TABLET.toString()))
Assert.assertEquals(DeviceType.DESKTOP, fromString(DeviceType.DESKTOP.toString()))
Assert.assertEquals(DeviceType.LAPTOP, fromString(DeviceType.LAPTOP.toString()))
Assert.assertEquals(DeviceType.TV, fromString(DeviceType.TV.toString()))
Assert.assertEquals(DeviceType.DESKTOP, fromString("invalid"))
}
// Basic paired device testing
@Test
@Throws(CertificateException::class)
fun testDevice() {
val device = Device(context, "testDevice")
Assert.assertEquals(device.deviceId, "testDevice")
Assert.assertEquals(device.deviceType, DeviceType.PHONE)
Assert.assertEquals(device.name, "Test Device")
Assert.assertTrue(device.isPaired)
Assert.assertNotNull(device.deviceInfo.certificate)
}
@Test
@Throws(CertificateException::class)
fun testPairingDone() {
val fakeNetworkPacket = NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY)
val deviceId = "unpairedTestDevice"
fakeNetworkPacket["deviceId"] = deviceId
fakeNetworkPacket["deviceName"] = "Unpaired Test Device"
fakeNetworkPacket["protocolVersion"] = DeviceHelper.ProtocolVersion
fakeNetworkPacket["deviceType"] = DeviceType.PHONE.toString()
val certificateString =
"""
MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2
NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u
bmVjdDAeFw0xNTA2MDMxMzE0MzhaFw0yNTA2MDMxMzE0MzhaMFUxLzAtBgNVBAMMJl9kYTE3OWE5
MV9mMDY0XzQ3OGVfYmU4Y18xOTM1ZDc1NDRkNTRfMQwwCgYDVQQKDANLREUxFDASBgNVBAsMC0tk
ZSBjb25uZWN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzH9GxS1lctpwYdSGAoPH
ws+MnVaL0PVDCuzrpxzXc+bChR87xofhQIesLPLZEcmUJ1MlEJ6jx4W+gVhvY2tUN7SoiKKbnq8s
WjI5ovs5yML3C1zPbOSJAdK613FcdkK+UGd/9dQk54gIozinC58iyTAChVVpB3pAF38EPxwKkuo2
qTzwk24d6PRxz1skkzwEphUQQzGboyHsAlJHN1MzM2/yFGB4l8iUua2d3ETyfy/xFEh/SwtGtXE5
KLz4cpb0fxjeYQZVruBKxzE07kgDO3zOhmP3LJ/KSPHWYImd1DWmpY9iDvoXr6+V7FAnRloaEIyg
7WwdlSCpo3TXVuIjLwIDAQABozIwMDAdBgNVHQ4EFgQUwmbHo8YbiR463GRKSLL3eIKyvDkwDwYD
VR0TAQH/BAUwAwIBADANBgkqhkiG9w0BAQUFAAOCAQEAydijH3rbnvpBDB/30w2PCGMT7O0N/XYM
wBtUidqa4NFumJrNrccx5Ehp4UP66BfP61HW8h2U/EekYfOsZyyWd4KnsDD6ycR8h/WvpK3BC2cn
I299wbqCEZmk5ZFFaEIDHdLAdgMCuxJkAzy9mMrWEa05Soxi2/ZXdrU9nXo5dzuPGYlirVPDHl7r
/urBxD6HVX3ObQJRJ7r/nAWyUVdX3/biJaDRsydftOpGU6Gi5c1JK4MWIz8Bsjh6mEjCsVatbPPl
yygGiJbDZfAvN2XoaVEBii2GDDCWfaFwPVPYlNTvjkUkMP8YThlMsiJ8Q4693XoLOL94GpNlCfUg
7n+KOQ==
""".trimIndent()
val certificateBytes = Base64.decode(certificateString, 0)
val certificate = SslHelper.parseCertificate(certificateBytes)
val deviceInfo = fromIdentityPacketAndCert(fakeNetworkPacket, certificate)
val linkProvider = Mockito.mock(LanLinkProvider::class.java)
Mockito.`when`(linkProvider.name).thenReturn("LanLinkProvider")
val link = Mockito.mock(LanLink::class.java)
Mockito.`when`(link.linkProvider).thenReturn(linkProvider)
Mockito.`when`(link.deviceId).thenReturn(deviceId)
Mockito.`when`(link.deviceInfo).thenReturn(deviceInfo)
val device = Device(context, link)
Assert.assertNotNull(device)
Assert.assertEquals(device.deviceId, deviceId)
Assert.assertEquals(device.name, "Unpaired Test Device")
Assert.assertEquals(device.deviceType, DeviceType.PHONE)
Assert.assertNotNull(device.deviceInfo.certificate)
device.pairingHandler.pairingDone()
Assert.assertTrue(device.isPaired)
val preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE)
Assert.assertTrue(preferences.getBoolean(device.deviceId, false))
val settings = context.getSharedPreferences(device.deviceId, Context.MODE_PRIVATE)
Assert.assertEquals(
settings.getString("deviceName", "Unknown device"),
"Unpaired Test Device"
)
Assert.assertEquals(settings.getString("deviceType", "tablet"), "phone")
// Cleanup for unpaired test device
preferences.edit().remove(device.deviceId).apply()
settings.edit().clear().apply()
}
@Test
@Throws(CertificateException::class)
fun testUnpair() {
val pairingCallback = Mockito.mock(PairingCallback::class.java)
val device = Device(context, "testDevice")
device.addPairingCallback(pairingCallback)
device.unpair()
Assert.assertFalse(device.isPaired)
val preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE)
Assert.assertFalse(preferences.getBoolean(device.deviceId, false))
Mockito.verify(pairingCallback, Mockito.times(1)).unpaired()
}
}

View File

@@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2024 TPJ Schikhof <kde@schikhof.eu>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Helpers
import org.junit.Assert
import org.junit.Test
import java.nio.file.Files
import kotlin.io.path.pathString
class FilesHelperTest {
@Test
fun findNonExistentName() {
val temporaryTestFolder = Files.createTempDirectory("kde_connect_file_unit_test")
val baseName = "example_file"
val firstFile = temporaryTestFolder.resolve("$baseName").toFile()
val secondFile = temporaryTestFolder.resolve("$baseName (1).").toFile()
firstFile.delete()
secondFile.delete()
// in case the test fails
firstFile.deleteOnExit()
secondFile.deleteOnExit()
val firstFileName = FilesHelper.findNonExistingNameForNewFile(temporaryTestFolder.pathString, baseName)
firstFile.createNewFile()
Assert.assertEquals("$baseName", firstFileName)
val secondFileName = FilesHelper.findNonExistingNameForNewFile(temporaryTestFolder.pathString, baseName)
Assert.assertEquals("$baseName (1).", secondFileName)
secondFile.createNewFile()
val thirdFileName = FilesHelper.findNonExistingNameForNewFile(temporaryTestFolder.pathString, baseName)
Assert.assertEquals("$baseName (2).", thirdFileName)
}
@Test
fun fileSystemSafeName() {
val notTooLong = (0..254).joinToString("") { "A" }
val tooLong = (0..255).joinToString("") { "A" }
Assert.assertEquals(notTooLong, FilesHelper.toFileSystemSafeName(notTooLong))
Assert.assertEquals(notTooLong, FilesHelper.toFileSystemSafeName(tooLong))
Assert.assertEquals("Averyspecialfile", FilesHelper.toFileSystemSafeName("A very special file \uD83E\uDD70"))
Assert.assertEquals("A_very_special_file", FilesHelper.toFileSystemSafeName("A_very_special_file \uD83E\uDD70"))
Assert.assertEquals("12345", FilesHelper.toFileSystemSafeName("1 2 3 4 5"))
Assert.assertEquals("1-2.3/4\\5", FilesHelper.toFileSystemSafeName("1-2.3/4\\5"))
Assert.assertEquals("___", FilesHelper.toFileSystemSafeName(" _ _ _ "))
}
// Other functions require Android classes
}

View File

@@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Daniel Weigl <DanielWeigl@gmx.at>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Helpers;
import junit.framework.TestCase;
public class SafeTextCheckerTest extends TestCase {
public void testSafeTextChecker() {
SafeTextChecker safeTextChecker = new SafeTextChecker("1234567890", 8);
assertIsOkay("123456", safeTextChecker);
assertIsOkay("123", safeTextChecker);
assertIsOkay("12345678", safeTextChecker);
assertIsOkay("", safeTextChecker);
assertIsNotOkay(null, safeTextChecker);
assertIsNotOkay("123456789", safeTextChecker);
assertIsNotOkay("123o", safeTextChecker);
assertIsNotOkay("O123", safeTextChecker); // its a O not a 0
assertIsNotOkay("o", safeTextChecker);
assertIsNotOkay(" ", safeTextChecker);
assertIsNotOkay("12345678 ", safeTextChecker);
}
private void assertIsOkay(String text, SafeTextChecker stc) {
assertTrue(text + " should be okay", stc.isSafe(text));
}
private void assertIsNotOkay(String text, SafeTextChecker stc) {
assertFalse(text + " should not be okay", stc.isSafe(text));
}
}

View File

@@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2021 Daniel Weigl <DanielWeigl@gmx.at>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Helpers
import org.junit.Assert
import org.junit.Test
class SafeTextCheckerTest {
@Test
fun testSafeTextChecker() {
val safeTextChecker = SafeTextChecker("1234567890", 8)
assertIsOkay("123456", safeTextChecker)
assertIsOkay("123", safeTextChecker)
assertIsOkay("12345678", safeTextChecker)
assertIsOkay("", safeTextChecker)
assertIsNotOkay(null, safeTextChecker)
assertIsNotOkay("123456789", safeTextChecker)
assertIsNotOkay("123o", safeTextChecker)
assertIsNotOkay("O123", safeTextChecker) // its a O not a 0
assertIsNotOkay("o", safeTextChecker)
assertIsNotOkay(" ", safeTextChecker)
assertIsNotOkay("12345678 ", safeTextChecker)
}
private fun assertIsOkay(text: String, stc: SafeTextChecker) = Assert.assertTrue("$text should be okay", stc.isSafe(text))
private fun assertIsNotOkay(text: String?, stc: SafeTextChecker) = Assert.assertFalse("$text should not be okay", stc.isSafe(text))
}

View File

@@ -1,159 +0,0 @@
package org.kde.kdeconnect;
import android.content.SharedPreferences;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Mock implementation of shared preference, which just saves data in memory using map.
*
* It DOES NOT support transactions, changes are immediate
*
* From https://gist.github.com/amardeshbd/354173d00b988574ee5019c4ba0c8a0b
*/
class MockSharedPreference implements SharedPreferences {
private final HashMap<String, Object> preferenceMap;
private final MockSharedPreferenceEditor preferenceEditor;
public MockSharedPreference() {
preferenceMap = new HashMap<>();
preferenceEditor = new MockSharedPreferenceEditor(preferenceMap);
}
@Override
public Map<String, ?> getAll() {
return preferenceMap;
}
@Nullable
@Override
public String getString(final String s, @Nullable final String def) {
if (!preferenceMap.containsKey(s)) return def;
return (String) preferenceMap.get(s);
}
@Nullable
@Override
public Set<String> getStringSet(final String s, @Nullable final Set<String> def) {
if (!preferenceMap.containsKey(s)) return def;
return (Set<String>) preferenceMap.get(s);
}
@Override
public int getInt(final String s, final int def) {
if (!preferenceMap.containsKey(s)) return def;
return (int) preferenceMap.get(s);
}
@Override
public long getLong(final String s, final long def) {
if (!preferenceMap.containsKey(s)) return def;
return (long) preferenceMap.get(s);
}
@Override
public float getFloat(final String s, final float def) {
if (!preferenceMap.containsKey(s)) return def;
return (float) preferenceMap.get(s);
}
@Override
public boolean getBoolean(final String s, final boolean def) {
if (!preferenceMap.containsKey(s)) return def;
return (boolean) preferenceMap.get(s);
}
@Override
public boolean contains(final String s) {
return preferenceMap.containsKey(s);
}
@Override
public Editor edit() {
return preferenceEditor;
}
@Override
public void registerOnSharedPreferenceChangeListener(final OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
}
@Override
public void unregisterOnSharedPreferenceChangeListener(final OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
}
static class MockSharedPreferenceEditor implements Editor {
private final HashMap<String, Object> preferenceMap;
MockSharedPreferenceEditor(final HashMap<String, Object> preferenceMap) {
this.preferenceMap = preferenceMap;
}
@Override
public Editor putString(final String s, @Nullable final String s1) {
preferenceMap.put(s, s1);
return this;
}
@Override
public Editor putStringSet(final String s, @Nullable final Set<String> set) {
preferenceMap.put(s, set);
return this;
}
@Override
public Editor putInt(final String s, final int i) {
preferenceMap.put(s, i);
return this;
}
@Override
public Editor putLong(final String s, final long l) {
preferenceMap.put(s, l);
return this;
}
@Override
public Editor putFloat(final String s, final float v) {
preferenceMap.put(s, v);
return this;
}
@Override
public Editor putBoolean(final String s, final boolean b) {
preferenceMap.put(s, b);
return this;
}
@Override
public Editor remove(final String s) {
preferenceMap.remove(s);
return this;
}
@Override
public Editor clear() {
preferenceMap.clear();
return this;
}
@Override
public boolean commit() {
return true;
}
@Override
public void apply() {
// Nothing to do, everything is saved in memory.
}
}
}

View File

@@ -0,0 +1,79 @@
package org.kde.kdeconnect
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
/**
* Mock implementation of shared preference, which just saves data in memory using map.
*
* It DOES NOT support transactions, changes are immediate
*
* From https://gist.github.com/amardeshbd/354173d00b988574ee5019c4ba0c8a0b
*/
internal class MockSharedPreference : SharedPreferences {
private val preferenceMap = mutableMapOf<String, Any?>()
private val preferenceEditor = MockSharedPreferenceEditor(preferenceMap)
override fun getAll(): Map<String, *> = preferenceMap
override fun getString(s: String, def: String?): String? = preferenceMap.getOrDefault(s, def) as String?
override fun getBoolean(s: String, def: Boolean): Boolean = preferenceMap.getOrDefault(s, def) as Boolean
override fun getInt(s: String, def: Int): Int = preferenceMap.getOrDefault(s, def) as Int
override fun getLong(s: String, def: Long): Long = preferenceMap.getOrDefault(s, def) as Long
override fun getFloat(s: String, def: Float): Float = preferenceMap.getOrDefault(s, def) as Float
override fun getStringSet(s: String, def: Set<String>?): Set<String>? = preferenceMap.getOrDefault(s, def) as Set<String>?
override fun contains(s: String): Boolean = preferenceMap.containsKey(s)
override fun edit(): SharedPreferences.Editor = preferenceEditor
override fun registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener: OnSharedPreferenceChangeListener) {}
override fun unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener: OnSharedPreferenceChangeListener) {}
internal class MockSharedPreferenceEditor(private val preferenceMap: MutableMap<String, Any?>) : SharedPreferences.Editor {
override fun putString(s: String, s1: String?): SharedPreferences.Editor {
preferenceMap[s] = s1
return this
}
override fun putStringSet(s: String, set: Set<String>?): SharedPreferences.Editor {
preferenceMap[s] = set
return this
}
override fun putInt(s: String, i: Int): SharedPreferences.Editor {
preferenceMap[s] = i
return this
}
override fun putLong(s: String, l: Long): SharedPreferences.Editor {
preferenceMap[s] = l
return this
}
override fun putFloat(s: String, v: Float): SharedPreferences.Editor {
preferenceMap[s] = v
return this
}
override fun putBoolean(s: String, b: Boolean): SharedPreferences.Editor {
preferenceMap[s] = b
return this
}
override fun remove(s: String): SharedPreferences.Editor {
preferenceMap.remove(s)
return this
}
override fun clear(): SharedPreferences.Editor {
preferenceMap.clear()
return this
}
override fun commit(): Boolean {
return true
}
override fun apply() {
// Nothing to do, everything is saved in memory.
}
}
}