mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-09-01 14:45:08 +00:00
Compare commits
129 Commits
work/remov
...
v1.33.4
Author | SHA1 | Date | |
---|---|---|---|
|
ed1dcdab52 | ||
|
e3ccfb1b25 | ||
|
4ae04ae060 | ||
|
801367458e | ||
|
26c72fae89 | ||
|
b981f9234b | ||
|
7f07e4658f | ||
|
f182d27ebe | ||
|
3b9d6ac61e | ||
|
6f24ac8d25 | ||
|
2891ec2092 | ||
|
fc1424c67c | ||
|
83efd6b355 | ||
|
05e14bb81c | ||
|
1236cbe4e3 | ||
|
5f9159a13f | ||
|
fbd9f8f216 | ||
|
e32b6b67e0 | ||
|
63a849b80a | ||
|
170bb5e717 | ||
|
f121e4982e | ||
|
937289730d | ||
|
624a9302fd | ||
|
e6f4b69464 | ||
|
2190c9cdaa | ||
|
864d44cb5b | ||
|
72e958a891 | ||
|
d4ab2ca6cf | ||
|
fd51ec7c14 | ||
|
28070954a6 | ||
|
e10f2496de | ||
|
95b4c08605 | ||
|
51d4de34c4 | ||
|
de2001bbe1 | ||
|
9c80cb9a40 | ||
|
0b03a66c37 | ||
|
6d66d69820 | ||
|
c0fc19baaa | ||
|
03ea5eae4c | ||
|
b373c28cdd | ||
|
6c8d22b1ed | ||
|
69adfbfbc2 | ||
|
f80e29538a | ||
|
56dda889d1 | ||
|
0c48f388f4 | ||
|
3eda9d4ef9 | ||
|
5aa6fae03b | ||
|
228a504b90 | ||
|
34c2c311e2 | ||
|
08fcfc863a | ||
|
c550ef4445 | ||
|
ec40994d4c | ||
|
43d4f38765 | ||
|
fbff23a8c0 | ||
|
52ff931c4e | ||
|
0f628d4927 | ||
|
737c333a91 | ||
|
e9e406de88 | ||
|
9b2e4bcf56 | ||
|
d3daf20c27 | ||
|
7de2817274 | ||
|
6121fa04bc | ||
|
bec807fa63 | ||
|
fe97750e9a | ||
|
f8a2d2da03 | ||
|
68a0b73e9c | ||
|
7a4fb8b584 | ||
|
b4ee6e30b1 | ||
|
0560071cfb | ||
|
4343ad7e01 | ||
|
0738710747 | ||
|
9af8fe791b | ||
|
a9a99ea7bd | ||
|
03c2121d57 | ||
|
c0c38aab9a | ||
|
ab4a6a300b | ||
|
82c434273d | ||
|
0e1842964f | ||
|
0a82d303e4 | ||
|
c275e26e00 | ||
|
d951e3faad | ||
|
144d292948 | ||
|
32d293eb8d | ||
|
3e2c077674 | ||
|
49d36d57a6 | ||
|
8b6d789c02 | ||
|
de73362624 | ||
|
7bc90fbe85 | ||
|
818b99774d | ||
|
13b09ffae8 | ||
|
56c96b686d | ||
|
3b4f5f83b2 | ||
|
fc18d8a10f | ||
|
d73236ab96 | ||
|
ecd4bec109 | ||
|
bb152c4900 | ||
|
49a9cd5ea7 | ||
|
e363a5875a | ||
|
55d3fd630c | ||
|
a85e6f8057 | ||
|
125c9d54b3 | ||
|
5b937313ff | ||
|
2ad9f8eeb1 | ||
|
b3d84f31f4 | ||
|
5ca96fc378 | ||
|
05f1cbe136 | ||
|
d02e5aabb5 | ||
|
ae24cd6ca8 | ||
|
16414401c0 | ||
|
3f120fbea8 | ||
|
97806cf6b0 | ||
|
a923deee58 | ||
|
0923c8ecda | ||
|
172822239c | ||
|
6a58cc444e | ||
|
26667e4b78 | ||
|
086d366a1c | ||
|
84d380aee5 | ||
|
1ea956f5fb | ||
|
cfc7242db5 | ||
|
93b257d46c | ||
|
fa22722498 | ||
|
b0c9e46a31 | ||
|
53b49163d5 | ||
|
444f5725af | ||
|
1104baca8f | ||
|
c3af9b03f6 | ||
|
ecb38f2518 | ||
|
75ddac0bf0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.attach_pid*
|
||||
local.properties
|
||||
/.gradle/
|
||||
/.idea/
|
||||
|
@@ -7,9 +7,7 @@ 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="13209"
|
||||
android:versionName="1.32.9">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
@@ -359,7 +357,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="org.kde.kdeconnect_tp.fileprovider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
|
@@ -21,37 +21,27 @@ buildscript {
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.kapt)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.dependencyLicenseReport)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
val licenseResDir = File("$projectDir/build/dependency-license-res")
|
||||
|
||||
fun String.runCommand(
|
||||
workingDir: File = File("."),
|
||||
timeoutAmount: Long = 60,
|
||||
timeoutUnit: TimeUnit = TimeUnit.SECONDS
|
||||
): String = ProcessBuilder(split("\\s(?=(?:[^'\"`]*(['\"`])[^'\"`]*\\1)*[^'\"`]*$)".toRegex()))
|
||||
.directory(workingDir)
|
||||
.redirectOutput(ProcessBuilder.Redirect.PIPE)
|
||||
.redirectError(ProcessBuilder.Redirect.PIPE)
|
||||
.start()
|
||||
.apply { waitFor(timeoutAmount, timeoutUnit) }
|
||||
.run {
|
||||
val error = errorStream.bufferedReader().readText().trim()
|
||||
if (error.isNotEmpty()) {
|
||||
throw Exception(error)
|
||||
}
|
||||
inputStream.bufferedReader().readText().trim()
|
||||
}
|
||||
val hashProvider = project.providers.exec {
|
||||
workingDir = rootDir
|
||||
commandLine("git", "rev-parse", "--short", "HEAD")
|
||||
}.standardOutput.asText.map { it.trim() }
|
||||
|
||||
android {
|
||||
namespace = "org.kde.kdeconnect_tp"
|
||||
compileSdk = 35
|
||||
defaultConfig {
|
||||
applicationId = "org.kde.kdeconnect_tp"
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 13304
|
||||
versionName = "1.33.4"
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
||||
}
|
||||
buildFeatures {
|
||||
@@ -74,16 +64,18 @@ android {
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.setSrcDirs(listOf("src"))
|
||||
resources.setSrcDirs(listOf("resources"))
|
||||
res.setSrcDirs(listOf(licenseResDir, "res"))
|
||||
assets.setSrcDirs(listOf("assets"))
|
||||
setRoot(".") // By default AGP expects all directories under src/main/...
|
||||
java.srcDir("src") // by default is "java"
|
||||
res.setSrcDirs(listOf(licenseResDir, "res")) // add licenseResDir
|
||||
}
|
||||
getByName("debug") {
|
||||
res.srcDir("dbg-res")
|
||||
}
|
||||
getByName("test") {
|
||||
java.setSrcDirs(listOf("tests"))
|
||||
java.srcDir("tests")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +97,8 @@ android {
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
@@ -127,8 +121,7 @@ android {
|
||||
// Default output filename is "${project.name}-${v.name}.apk". We want
|
||||
// the Git commit short-hash to be added onto that default filename.
|
||||
try {
|
||||
val hash = "git rev-parse --short HEAD".runCommand(workingDir = rootDir)
|
||||
val newName = "${project.name}-${variant.name}-${hash}.apk"
|
||||
val newName = "${project.name}-${variant.name}-${hashProvider.get()}.apk"
|
||||
logger.quiet(" Found an output file ${output.outputFile.name}, renaming to $newName")
|
||||
output.outputFileName = newName
|
||||
} catch (ignored: Exception) {
|
||||
@@ -254,6 +247,10 @@ abstract class FixCollectionsClassVisitorFactory :
|
||||
interface Params : InstrumentationParameters
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("com.albertvaka.classindexksp.annotations", "org.kde.kdeconnect.Plugins.PluginFactory.LoadablePlugin")
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
variant.instrumentation.transformClassesWith(
|
||||
@@ -304,11 +301,9 @@ dependencies {
|
||||
implementation(libs.apache.sshd.mina)
|
||||
implementation(libs.apache.mina.core)
|
||||
|
||||
//implementation("com.github.bright:slf4android:0.1.6") { transitive = true } // For org.apache.sshd debugging
|
||||
implementation(libs.bcpkix.jdk15on) //For SSL certificate generation
|
||||
|
||||
implementation(libs.classindex)
|
||||
kapt(libs.classindex)
|
||||
ksp(libs.classindexksp)
|
||||
|
||||
// The android-smsmms library is the only way I know to handle MMS in Android
|
||||
// (Shouldn't a phone OS make phone things easy?)
|
||||
|
6
dbg-res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
6
dbg-res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- <background android:drawable="@drawable/ic_launcher_background"/>-->
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
5
dbg-res/mipmap-anydpi-v26/ic_launcher_banner.xml
Normal file
5
dbg-res/mipmap-anydpi-v26/ic_launcher_banner.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- <background android:drawable="@drawable/ic_launcher_banner_background"/>-->
|
||||
<foreground android:drawable="@drawable/ic_launcher_banner_foreground"/>
|
||||
</adaptive-icon>
|
6
dbg-res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
6
dbg-res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- <background android:drawable="@drawable/ic_launcher_background"/>-->
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
5
dbg-res/values-en-rGB/strings.xml
Normal file
5
dbg-res/values-en-rGB/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
<string name="kde_connect">Debug KDE Connect</string>
|
||||
</resources>
|
5
dbg-res/values/strings.xml
Normal file
5
dbg-res/values/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
<string name="kde_connect">Debug KDE Connect</string>
|
||||
</resources>
|
21
fastlane/metadata/android/ar/full_description.txt
Normal file
21
fastlane/metadata/android/ar/full_description.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
يوفر كِيدِي المتّصل مجموعة من الميزات لدمج سير عملك عبر الأجهزة:
|
||||
|
||||
- نقل الملفات بين أجهزتك.
|
||||
- الوصول إلى الملفات الموجودة على هاتفك من جهاز الكمبيوتر الخاص بك، دون أسلاك.
|
||||
- الحافظة المشتركة: النسخ واللصق بين أجهزتك.
|
||||
- الحصول على إشعارات للمكالمات والرسائل الواردة على جهاز الكمبيوتر الخاص بك.
|
||||
- لوحة اللمس الافتراضية: استخدم شاشة هاتفك كلوحة لمس لجهاز الكمبيوتر الخاص بك.
|
||||
- مزامنة الإشعارات: الوصول إلى إشعارات هاتفك من جهاز الكمبيوتر الخاص بك والرد على الرسائل.
|
||||
- التحكم عن بعد في الوسائط المتعددة: استخدم هاتفك كجهاز تحكم عن بعد لمشغلات الوسائط لينكس.
|
||||
- اتصال WiFi: لا حاجة إلى سلك USB أو بلوتوث.
|
||||
- تشفير TLS من البداية إلى النهاية: معلوماتك آمنة.
|
||||
|
||||
يرجى ملاحظة أنك ستحتاج إلى تثبيت كِيدِي المتّصل على حاسوبك حتى يعمل هذا التطبيق، والحفاظ على تحديث إصدار سطح المكتب بإصدار أندوريد حتى تعمل أحدث الميزات.
|
||||
|
||||
معلومات الأذونات الحساسة:
|
||||
* إذن إمكانية الوصول: مطلوب لتلقي إدخال من جهاز آخر للتحكم في هاتف أندرويد خاص بك، إذا كنت تستخدم ميزة الإدخال عن بُعد.
|
||||
* إذن تحديد الموقع في الخلفية: مطلوب لمعرفة شبكة واي فاي التي تتصل بها، إذا كنت تستخدم ميزة الشبكات الموثوقة.
|
||||
|
||||
لا يرسل كِيدِي المتّصل أي معلومات إلى كيدي أو إلى أي طرف ثالث. يرسل كِيدِي المتّصل البيانات من جهاز إلى آخر مباشرةً باستخدام الشبكة المحلية، وليس عبر الإنترنت، وباستخدام التشفير من البداية إلى النهاية.
|
||||
|
||||
هذا التطبيق جزء من مشروع مفتوح المصدر وهو موجود بفضل جميع الأشخاص الذين ساهموا فيه. قم بزيارة الموقع الإلكتروني للحصول على الكود المصدر.
|
1
fastlane/metadata/android/ar/short_description.txt
Normal file
1
fastlane/metadata/android/ar/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
يقوم كِيدِي المتّصل بدمج هاتفك الذكي والحاسوب
|
1
fastlane/metadata/android/ar/title.txt
Normal file
1
fastlane/metadata/android/ar/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect
|
14
fastlane/metadata/android/en-US/changelogs/13210.txt
Normal file
14
fastlane/metadata/android/en-US/changelogs/13210.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
1.32.10
|
||||
* Fixed app showing behind the notifications bar in Android 15
|
||||
* Fixed file transfers showing as failed when they succeeded
|
||||
* Fixed plugin list not updating after granting permissions
|
||||
|
||||
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
|
6
fastlane/metadata/android/en-US/changelogs/13300.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/13300.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
1.33.0
|
||||
* Add support for PeerTube links
|
||||
* Allow filtering notifications from work profile
|
||||
* Fix bug where devices would unpair without user interaction
|
||||
* Verification key now changes every second (only if both devices support it)
|
||||
* Fix crashes
|
9
fastlane/metadata/android/en-US/changelogs/13301.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/13301.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
1.33.1
|
||||
* Fix compatibility with GSConnect
|
||||
|
||||
1.33.0
|
||||
* Add support for PeerTube links
|
||||
* Allow filtering notifications from work profile
|
||||
* Fix bug where devices would unpair without user interaction
|
||||
* Verification key now changes every second (only if both devices support it)
|
||||
* Fix crashes
|
12
fastlane/metadata/android/en-US/changelogs/13302.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/13302.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
1.33.2
|
||||
* Fix connection issues on some devices
|
||||
|
||||
1.33.1
|
||||
* Fix compatibility with GSConnect
|
||||
|
||||
1.33.0
|
||||
* Add support for PeerTube links
|
||||
* Allow filtering notifications from work profile
|
||||
* Fix bug where devices would unpair without user interaction
|
||||
* Verification key now changes every second (only if both devices support it)
|
||||
* Fix crashes
|
16
fastlane/metadata/android/en-US/changelogs/13303.txt
Normal file
16
fastlane/metadata/android/en-US/changelogs/13303.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
1.33.3
|
||||
* Fix more connection issues. Pairing again might be needed in some setups.
|
||||
* Add a setting to export the application logs.
|
||||
|
||||
1.33.2
|
||||
* Fix connection issues on some devices.
|
||||
|
||||
1.33.1
|
||||
* Fix compatibility with GSConnect.
|
||||
|
||||
1.33.0
|
||||
* Add support for PeerTube links.
|
||||
* Allow filtering notifications from work profile.
|
||||
* Fix bug where devices would unpair without user interaction.
|
||||
* Verification key now changes every second (only if both devices support it).
|
||||
* Fix crashes.
|
16
fastlane/metadata/android/en-US/changelogs/13304.txt
Normal file
16
fastlane/metadata/android/en-US/changelogs/13304.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
1.33.4
|
||||
* Extend offline URL sharing behavior to direct share targets.
|
||||
* Improve paring screen.
|
||||
|
||||
1.33.3
|
||||
* Fix connection issues. Pairing again might be needed in some cases.
|
||||
* Add a setting to export the application logs.
|
||||
|
||||
1.33.1
|
||||
* Fix compatibility with GSConnect.
|
||||
|
||||
1.33.0
|
||||
* Add support for PeerTube links.
|
||||
* Allow filtering notifications from work profile.
|
||||
* Fix bug where devices would unpair without user interaction.
|
||||
* Verification key now changes every second (if both devices support it).
|
@@ -1,40 +1,41 @@
|
||||
[versions]
|
||||
activityCompose = "1.9.3"
|
||||
androidDesugarJdkLibs = "2.1.3"
|
||||
androidGradlePlugin = "8.7.3"
|
||||
activityCompose = "1.10.1"
|
||||
androidDesugarJdkLibs = "2.1.5"
|
||||
androidGradlePlugin = "8.9.2"
|
||||
androidSmsmms = "kdeconnect-1-21-0"
|
||||
appcompat = "1.7.0"
|
||||
bcpkixJdk15on = "1.70"
|
||||
classindex = "3.13"
|
||||
commonsCollections4 = "4.4"
|
||||
commonsIo = "2.17.0"
|
||||
classindexksp = "1.2"
|
||||
commonsCollections4 = "4.5.0"
|
||||
commonsIo = "2.19.0"
|
||||
commonsLang3 = "3.17.0"
|
||||
constraintlayoutCompose = "1.1.0"
|
||||
coreKtx = "1.15.0"
|
||||
dependencyLicenseReport = "2.7"
|
||||
constraintlayoutCompose = "1.1.1"
|
||||
coreKtx = "1.16.0"
|
||||
dependencyLicenseReport = "2.9"
|
||||
disklrucache = "2.0.2"
|
||||
documentfile = "1.0.1"
|
||||
gridlayout = "1.0.0"
|
||||
gridlayout = "1.1.0"
|
||||
jsonassert = "1.5.3"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.0.21"
|
||||
kotlinxCoroutinesCore = "1.9.0"
|
||||
kotlin = "2.1.20"
|
||||
kspPlugin = "2.1.20-1.0.32"
|
||||
kotlinxCoroutinesCore = "1.10.2"
|
||||
lifecycleExtensions = "2.2.0"
|
||||
lifecycleRuntimeKtx = "2.8.7"
|
||||
logger = "1.0.3"
|
||||
material = "1.12.0"
|
||||
material3 = "1.3.1"
|
||||
material3 = "1.3.2"
|
||||
media = "1.7.0"
|
||||
minaCore = "2.2.3"
|
||||
mockitoCore = "5.14.2"
|
||||
minaCore = "2.2.4"
|
||||
mockitoCore = "5.17.0"
|
||||
preferenceKtx = "1.2.1"
|
||||
reactiveStreams = "1.0.4"
|
||||
recyclerview = "1.3.2"
|
||||
recyclerview = "1.4.0"
|
||||
rxjava = "2.2.21"
|
||||
sl4j = "2.0.13"
|
||||
sshdCore = "2.14.0"
|
||||
sshdCore = "2.15.0"
|
||||
swiperefreshlayout = "1.1.0"
|
||||
uiToolingPreview = "1.7.5"
|
||||
uiToolingPreview = "1.8.0"
|
||||
univocityParsers = "2.9.1"
|
||||
|
||||
[libraries]
|
||||
@@ -58,7 +59,7 @@ androidx-preference-ktx = { module = "androidx.preference:preference-ktx", versi
|
||||
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
||||
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
|
||||
bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkixJdk15on" }
|
||||
classindex = { module = "org.atteo.classindex:classindex", version.ref = "classindex" }
|
||||
classindexksp = { module = "com.github.albertvaka:classindexksp", version.ref = "classindexksp" }
|
||||
commons-collections4 = { module = "org.apache.commons:commons-collections4", version.ref = "commonsCollections4" }
|
||||
commons-io = { module = "commons-io:commons-io", version.ref = "commonsIo" }
|
||||
commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" }
|
||||
@@ -87,5 +88,5 @@ slf4j-handroid = { group = "com.gitlab.mvysny.slf4j", name = "slf4j-handroid", v
|
||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "kspPlugin" }
|
||||
dependencyLicenseReport = { id = "com.github.jk1.dependency-license-report", version.ref = "dependencyLicenseReport" }
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,7 @@
|
||||
#Sat Sep 28 01:39:16 AM EDT 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
84
po/ar/kdeconnect-android-store-full.po
Normal file
84
po/ar/kdeconnect-android-store-full.po
Normal file
@@ -0,0 +1,84 @@
|
||||
# SPDX-FileCopyrightText: 2025 Zayed Al-Saidi <zayed.alsaidi@gmail.com>
|
||||
#. 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: 2025-02-09 17:40+0400\n"
|
||||
"Last-Translator: Zayed Al-Saidi <zayed.alsaidi@gmail.com>\n"
|
||||
"Language-Team: ar\n"
|
||||
"Language: ar\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
||||
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||
"X-Generator: Lokalize 23.08.5\n"
|
||||
|
||||
msgid ""
|
||||
"KDE Connect provides a set of features to integrate your workflow across "
|
||||
"devices:\n"
|
||||
"\n"
|
||||
"- Transfer files between your devices.\n"
|
||||
"- Access files on your phone from your computer, without wires.\n"
|
||||
"- Shared clipboard: copy and paste between your devices.\n"
|
||||
"- Get notifications for incoming calls and messages on your computer.\n"
|
||||
"- Virtual touchpad: Use your phone screen as your computer's touchpad.\n"
|
||||
"- Notifications sync: Access your phone notifications from your computer and "
|
||||
"reply to messages.\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"
|
||||
msgstr ""
|
||||
"يوفر كِيدِي المتّصل مجموعة من الميزات لدمج سير عملك عبر الأجهزة:\n"
|
||||
"\n"
|
||||
"- نقل الملفات بين أجهزتك.\n"
|
||||
"- الوصول إلى الملفات الموجودة على هاتفك من جهاز الكمبيوتر الخاص بك، دون "
|
||||
"أسلاك.\n"
|
||||
"- الحافظة المشتركة: النسخ واللصق بين أجهزتك.\n"
|
||||
"- الحصول على إشعارات للمكالمات والرسائل الواردة على جهاز الكمبيوتر الخاص "
|
||||
"بك.\n"
|
||||
"- لوحة اللمس الافتراضية: استخدم شاشة هاتفك كلوحة لمس لجهاز الكمبيوتر الخاص "
|
||||
"بك.\n"
|
||||
"- مزامنة الإشعارات: الوصول إلى إشعارات هاتفك من جهاز الكمبيوتر الخاص بك "
|
||||
"والرد على الرسائل.\n"
|
||||
"- التحكم عن بعد في الوسائط المتعددة: استخدم هاتفك كجهاز تحكم عن بعد لمشغلات "
|
||||
"الوسائط لينكس.\n"
|
||||
"- اتصال WiFi: لا حاجة إلى سلك USB أو بلوتوث.\n"
|
||||
"- تشفير TLS من البداية إلى النهاية: معلوماتك آمنة.\n"
|
||||
"\n"
|
||||
"يرجى ملاحظة أنك ستحتاج إلى تثبيت كِيدِي المتّصل على حاسوبك حتى يعمل هذا "
|
||||
"التطبيق، والحفاظ على تحديث إصدار سطح المكتب بإصدار أندوريد حتى تعمل أحدث "
|
||||
"الميزات.\n"
|
||||
"\n"
|
||||
"معلومات الأذونات الحساسة:\n"
|
||||
"* إذن إمكانية الوصول: مطلوب لتلقي إدخال من جهاز آخر للتحكم في هاتف أندرويد "
|
||||
"خاص بك، إذا كنت تستخدم ميزة الإدخال عن بُعد.\n"
|
||||
"* إذن تحديد الموقع في الخلفية: مطلوب لمعرفة شبكة واي فاي التي تتصل بها، إذا "
|
||||
"كنت تستخدم ميزة الشبكات الموثوقة.\n"
|
||||
"\n"
|
||||
"لا يرسل كِيدِي المتّصل أي معلومات إلى كيدي أو إلى أي طرف ثالث. يرسل كِيدِي المتّصل "
|
||||
"البيانات من جهاز إلى آخر مباشرةً باستخدام الشبكة المحلية، وليس عبر الإنترنت، "
|
||||
"وباستخدام التشفير من البداية إلى النهاية.\n"
|
||||
"\n"
|
||||
"هذا التطبيق جزء من مشروع مفتوح المصدر وهو موجود بفضل جميع الأشخاص الذين "
|
||||
"ساهموا فيه. قم بزيارة الموقع الإلكتروني للحصول على الكود المصدر.\n"
|
20
po/ar/kdeconnect-android-store-short.po
Normal file
20
po/ar/kdeconnect-android-store-short.po
Normal file
@@ -0,0 +1,20 @@
|
||||
# SPDX-FileCopyrightText: 2025 Zayed Al-Saidi <zayed.alsaidi@gmail.com>
|
||||
#. extracted from ./metadata/android/en-US/short_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: 2025-02-09 17:40+0400\n"
|
||||
"Last-Translator: Zayed Al-Saidi <zayed.alsaidi@gmail.com>\n"
|
||||
"Language-Team: ar\n"
|
||||
"Language: ar\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
||||
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||
"X-Generator: Lokalize 23.08.5\n"
|
||||
|
||||
msgid "KDE Connect integrates your smartphone and computer"
|
||||
msgstr "يقوم كِيدِي المتّصل بدمج هاتفك الذكي والحاسوب"
|
25
proguard-rules.pro
vendored
25
proguard-rules.pro
vendored
@@ -17,29 +17,10 @@
|
||||
#}
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
-dontwarn org.spongycastle.**
|
||||
-dontwarn org.apache.sshd.**
|
||||
-dontwarn org.apache.mina.**
|
||||
-dontwarn org.slf4j.**
|
||||
-dontwarn io.netty.**
|
||||
|
||||
-keepattributes SourceFile,LineNumberTable,Signature,*Annotation*
|
||||
|
||||
-keep class org.spongycastle.** {*;}
|
||||
|
||||
# SSHd requires mina, and mina uses reflection so some classes would get deleted
|
||||
-keep class org.apache.mina.** {*;}
|
||||
-keep class org.apache.sshd.** {*;}
|
||||
|
||||
-keep class org.kde.kdeconnect.** {*;}
|
||||
|
||||
-dontwarn org.mockito.**
|
||||
-dontwarn sun.reflect.**
|
||||
-dontwarn android.test.**
|
||||
-dontwarn java.lang.management.**
|
||||
-dontwarn javax.**
|
||||
# SSHd requires mina, and mina uses reflection so some classes would get deleted
|
||||
-keep class org.apache.sshd.** {*;}
|
||||
-dontwarn org.apache.sshd.**
|
||||
|
||||
-dontwarn android.net.ConnectivityManager
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-dontwarn android.net.LinkProperties
|
||||
|
25
res/layout-v23/pairing_explanation_duplicate_names.xml
Normal file
25
res/layout-v23/pairing_explanation_duplicate_names.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:drawablePadding="8dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:text="@string/pairing_duplicate_names"
|
||||
app:drawableStartCompat="@drawable/ic_warning"
|
||||
app:drawableTint="?attr/colorControlNormal">
|
||||
|
||||
</TextView>
|
@@ -21,6 +21,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:clipToPadding="false"
|
||||
android:id="@+id/scroll_view"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -26,6 +26,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/custom_device_item"/>
|
||||
|
||||
<TextView
|
||||
|
@@ -30,7 +30,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:id="@+id/device_view"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<!-- Shown when the device is paired and reachable -->
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
|
@@ -20,5 +20,6 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:id="@+id/licenses_text"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"/>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@@ -17,7 +17,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<!-- Keep in sync with toolbar.xml, copied here because it needs the nested TabLayout -->
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
|
@@ -57,6 +57,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:dividerHeight="0dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:clipToPadding="false"
|
||||
tools:context=".MainActivity" />
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@@ -24,7 +24,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
app:drawableEndCompat="@drawable/ic_delete"
|
||||
app:drawableStartCompat="@drawable/ic_delete" />
|
||||
|
||||
<FrameLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/swipeableView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -32,17 +32,30 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deviceNameOrIP"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingRight"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||
android:visibility="visible"
|
||||
tools:text="192.168.0.1"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="192.168.0.1" />
|
||||
|
||||
</FrameLayout>
|
||||
<TextView
|
||||
android:id="@+id/connectionStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingRight"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
@@ -22,6 +22,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:dividerHeight="12dp"
|
||||
android:orientation="vertical"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/list_card_entry"
|
||||
tools:context=".MainActivity" />
|
||||
|
||||
|
@@ -11,7 +11,9 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fillViewport="true">
|
||||
android:fillViewport="true"
|
||||
android:clipToPadding="false"
|
||||
android:id="@+id/scroll_view">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/about_layout"
|
||||
|
25
res/layout/pairing_explanation_duplicate_names.xml
Normal file
25
res/layout/pairing_explanation_duplicate_names.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:drawablePadding="8dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:text="@string/pairing_duplicate_names"
|
||||
app:drawableStartCompat="@drawable/ic_warning"
|
||||
app:drawableLeftCompat="@drawable/ic_warning">
|
||||
|
||||
</TextView>
|
@@ -19,12 +19,6 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pair_progress"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pair_message"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -33,6 +27,13 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:text="@string/device_not_paired"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pairing_explanation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dip"
|
||||
android:text="@string/pairing_explanation" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pair_verification"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -43,6 +44,12 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
app:drawableStartCompat="@drawable/ic_key" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pair_progress"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pair_button"
|
||||
android:layout_width="match_parent"
|
||||
|
@@ -17,6 +17,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_mpris">تحكّمات الوسائط المتعدّدة</string>
|
||||
<string name="pref_plugin_mpris_desc">توفّر تحكّمًا بعيدًا لمشغّل الوسائط</string>
|
||||
<string name="pref_plugin_runcommand">شغّل أمرًا</string>
|
||||
@@ -76,6 +77,7 @@
|
||||
<string name="device_menu_plugins">إعدادات الملحقة</string>
|
||||
<string name="device_menu_unpair">ألغِ الاقتران</string>
|
||||
<string name="pair_new_device">اقرن جهازًا جديدًا</string>
|
||||
<string name="cancel_pairing">ألغ الاقتران</string>
|
||||
<string name="unknown_device">جهاز مجهول</string>
|
||||
<string name="error_not_reachable">الجهاز غير قابل الوصول</string>
|
||||
<string name="error_already_paired">الجهاز مقترن بالفعل</string>
|
||||
@@ -83,8 +85,11 @@
|
||||
<string name="error_canceled_by_user">ألغاه المستخدم</string>
|
||||
<string name="error_canceled_by_other_peer">ألغاه ندّ آخر</string>
|
||||
<string name="encryption_info_title">معلومات التّعمية</string>
|
||||
<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>
|
||||
<string name="tap_to_open">اطرق لتفتح</string>
|
||||
<string name="received_file_text">المس لفتح \'%1s\'</string>
|
||||
<string name="tap_to_answer">المس للإجابة</string>
|
||||
@@ -173,6 +178,7 @@
|
||||
<string name="clipboard_toast">نُسخ إلى الحافظة</string>
|
||||
<string name="runcommand_notreachable">الجهاز غير قابل الوصول</string>
|
||||
<string name="runcommand_notpaired">الجهاز غير مقترن</string>
|
||||
<string name="runcommand_category_device_controls_title">متحكمات الجهاز</string>
|
||||
<string name="pref_plugin_findremotedevice">اعثر على جهاز بعيد</string>
|
||||
<string name="pref_plugin_findremotedevice_desc">رنّ على الجهاز البعيد</string>
|
||||
<string name="ring">رّن</string>
|
||||
@@ -183,6 +189,8 @@
|
||||
<string name="settings_rename">اسم الجهاز</string>
|
||||
<string name="settings_dark_mode">سمة مظلمة</string>
|
||||
<string name="settings_more_settings_title">المزيد من الإعدادات</string>
|
||||
<string name="setting_persistent_notification_oreo">الإخطارات المستمرّة</string>
|
||||
<string name="extra_options">الخيارات الإضافية</string>
|
||||
<string name="privacy_options">خيارات الخصوصية</string>
|
||||
<string name="set_privacy_options">حدد خيارات الخصوصية</string>
|
||||
<string name="block_contents">امنح محتويات الإخطارات</string>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Устройството е недостъпно</string>
|
||||
<string name="error_already_paired">Устройството вече е сдвоено</string>
|
||||
<string name="error_timed_out">Просрочка</string>
|
||||
<string name="error_clocks_not_match">Часовниците на устройствата не са синхронизирани</string>
|
||||
<string name="error_canceled_by_user">Отхвърлена от потребителя</string>
|
||||
<string name="error_canceled_by_other_peer">Отказана от другата страна</string>
|
||||
<string name="encryption_info_title">Информация за криптиране</string>
|
||||
@@ -192,11 +193,13 @@
|
||||
<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">Версия на протокол:</string>
|
||||
<string name="protocol_version_newer">Това устройство използва по-нова версия на протокола</string>
|
||||
<string name="plugin_settings_with_name">%s настройки</string>
|
||||
<string name="invalid_device_name">Невалидно име на устройство</string>
|
||||
<string name="shareplugin_text_saved">Получен текст, запазен в клипборда</string>
|
||||
<string name="custom_devices_settings">Списък с потребителски устройства</string>
|
||||
<string name="custom_devices_settings_summary">%d устройства, добавени ръчно</string>
|
||||
<string name="custom_device_list">Добавяне на устройства по IP адрес</string>
|
||||
<string name="custom_device_deleted">Изтрито потребителско устройство</string>
|
||||
<string name="custom_device_list_help">Ако устройството ви не е открито автоматично, можете да добавите неговия IP адрес или име на хост, като щракнете върху плаващия бутон за действие</string>
|
||||
@@ -306,6 +309,8 @@
|
||||
<string name="devices">Устройства</string>
|
||||
<string name="settings_rename">Име на устройство</string>
|
||||
<string name="settings_dark_mode">Тъмна тема</string>
|
||||
<string name="settings_export_logs">Експортиране на дневниците на KDE Connect</string>
|
||||
<string name="settings_export_logs_text">Генерирайте файл с информация за изпълнени команди, която може да помогне за отстраняването на проблеми.</string>
|
||||
<string name="settings_more_settings_title">Още настройки</string>
|
||||
<string name="settings_more_settings_text">Настройките за всяко устройство могат да бъдат намерени в раздел \"Настройки на плъгина\" в рамките на устройство.</string>
|
||||
<string name="setting_persistent_notification">Показване на постоянна нотификация</string>
|
||||
@@ -420,4 +425,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Продължаване на възпроизвеждането</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Показване на безшумно известие за продължаване на възпроизвеждането на това устройство след затваряне на медия</string>
|
||||
<string name="notification_channel_keepwatching">Продължаване на възпроизвеждането</string>
|
||||
<string name="ping_result">Извършен е пинг за %1$d милисекунди</string>
|
||||
<string name="ping_failed">Устройството не можа да изпрати пинг</string>
|
||||
<string name="ping_in_progress">Изпращане на пинг…</string>
|
||||
<string name="device_host_invalid">Хостът е невалиден. Използвайте валидно име на хост, IPv4 или IPv6</string>
|
||||
<string name="device_host_duplicate">Хостът вече е в списъка</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">No es pot accedir al dispositiu</string>
|
||||
<string name="error_already_paired">El dispositiu ja està aparellat</string>
|
||||
<string name="error_timed_out">Ha excedit el temps</string>
|
||||
<string name="error_clocks_not_match">Els rellotges dels dispositius no estan sincronitzats</string>
|
||||
<string name="error_canceled_by_user">Cancel·lat per l\'usuari</string>
|
||||
<string name="error_canceled_by_other_peer">Cancel·lat per l\'altre parell</string>
|
||||
<string name="encryption_info_title">Informació de l\'encriptatge</string>
|
||||
@@ -164,9 +165,11 @@
|
||||
<string name="middle_click">Envia un clic del mig</string>
|
||||
<string name="show_keyboard">Mostra el teclat</string>
|
||||
<string name="device_not_paired">El dispositiu no està aparellat</string>
|
||||
<string name="pairing_duplicate_names">Precaució: hi ha diversos dispositius amb el mateix nom.</string>
|
||||
<string name="request_pairing">Demana aparellar</string>
|
||||
<string name="pairing_accept">Accepta</string>
|
||||
<string name="pairing_reject">Rebutja</string>
|
||||
<string name="pairing_explanation">L\'aparellament de dos dispositius els donarà accés l\'un a l\'altre. Només aparelleu els vostres propis dispositius.</string>
|
||||
<string name="settings">Arranjament</string>
|
||||
<string name="mpris_play">Reprodueix</string>
|
||||
<string name="mpris_pause">Pausa</string>
|
||||
@@ -191,12 +194,15 @@
|
||||
<string name="mpris_notification_settings_summary">Permet controlar els reproductors multimèdia sense obrir el KDE Connect</string>
|
||||
<string name="share_to">Comparteix amb…</string>
|
||||
<string name="unreachable_device">%s (no s\'hi pot accedir)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="unreachable_device_url_share_text">Els URL compartits amb un dispositiu no accessible es lliuraran un cop s\'hi pugui accedir.\n\n</string>
|
||||
<string name="protocol_version">Versió del protocol:</string>
|
||||
<string name="protocol_version_newer">Aquest dispositiu usa una versió nova del protocol</string>
|
||||
<string name="plugin_settings_with_name">Configuració del %s</string>
|
||||
<string name="invalid_device_name">El nom del dispositiu no és vàlid</string>
|
||||
<string name="shareplugin_text_saved">S\'ha rebut text i s\'ha desat al porta-retalls</string>
|
||||
<string name="custom_devices_settings">Llista personalitzada de dispositius</string>
|
||||
<string name="custom_devices_settings_summary">S\'han afegir %d dispositius manualment</string>
|
||||
<string name="custom_device_list">Afegeix dispositius per la IP</string>
|
||||
<string name="custom_device_deleted">S\'ha suprimit un dispositiu personalitzat</string>
|
||||
<string name="custom_device_list_help">Si el dispositiu no es detecta automàticament, podeu afegir la seva adreça IP o el nom de la màquina fent clic al botó flotant d\'acció</string>
|
||||
@@ -306,6 +312,8 @@
|
||||
<string name="devices">Dispositius</string>
|
||||
<string name="settings_rename">Nom del dispositiu</string>
|
||||
<string name="settings_dark_mode">Tema fosc</string>
|
||||
<string name="settings_export_logs">Exporta els registres del KDE Connect</string>
|
||||
<string name="settings_export_logs_text">Genera un fitxer amb informació d\'execució que pot ajudar a resoldre problemes.</string>
|
||||
<string name="settings_more_settings_title">Més opcions</string>
|
||||
<string name="settings_more_settings_text">La configuració per dispositiu es pot trobar a «Arranjament dels connectors» des d\'un dispositiu.</string>
|
||||
<string name="setting_persistent_notification">Mostra les notificacions persistents</string>
|
||||
@@ -420,4 +428,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Continua reproduint</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Mostra una notificació silenciosa per a continuar reproduint en aquest dispositiu després de tancar l\'element multimèdia</string>
|
||||
<string name="notification_channel_keepwatching">Continua reproduint</string>
|
||||
<string name="ping_result">S\'ha fet «ping» en %1$d mil·lisegons</string>
|
||||
<string name="ping_failed">No s\'ha pogut fer «ping» al dispositiu</string>
|
||||
<string name="ping_in_progress">S\'està fent «ping»…</string>
|
||||
<string name="device_host_invalid">L\'ordinador no és vàlid. Useu un nom d\'ordinador, IPv4 o IPv6 vàlids</string>
|
||||
<string name="device_host_duplicate">L\'ordinador ja existeix a la llista</string>
|
||||
</resources>
|
||||
|
@@ -17,7 +17,7 @@
|
||||
<string name="pref_plugin_clipboard_desc">Kunhavigi la enhavon de la tondujo</string>
|
||||
<string name="pref_plugin_clipboard_sent">Clipboard Sendita</string>
|
||||
<string name="pref_plugin_mousepad">Fora enigo</string>
|
||||
<string name="pref_plugin_mousepad_desc">Uzu vian telefonon aŭ tablojdon kiel tuŝplaton kaj klavaron</string>
|
||||
<string name="pref_plugin_mousepad_desc">Uzu vian telefonon aŭ tabuleton kiel tuŝplaton kaj klavaron</string>
|
||||
<string name="pref_plugin_presenter">Prezento fora</string>
|
||||
<string name="pref_plugin_presenter_desc">Uzu vian aparaton por ŝanĝi lumbildojn en prezento</string>
|
||||
<string name="pref_plugin_remotekeyboard">Ricevi forajn klavojn</string>
|
||||
@@ -25,7 +25,7 @@
|
||||
<string name="pref_plugin_mpris">Plurmedia regiloj</string>
|
||||
<string name="pref_plugin_mpris_desc">Provizas teleregilon por via plurmedia ludilo</string>
|
||||
<string name="pref_plugin_runcommand">Lanĉi Komandon</string>
|
||||
<string name="pref_plugin_runcommand_desc">Ekigi forajn komandojn de via telefono aŭ tablojdo</string>
|
||||
<string name="pref_plugin_runcommand_desc">Ekigi forajn komandojn de via telefono aŭ tabuleto</string>
|
||||
<string name="pref_plugin_contacts">Sinkronigilo de Kontaktoj</string>
|
||||
<string name="pref_plugin_contacts_desc">Permesi sinkronigi la kontaktlibron de la aparato</string>
|
||||
<string name="pref_plugin_ping">Ping</string>
|
||||
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Aparato ne atingebla</string>
|
||||
<string name="error_already_paired">Aparato jam parigita</string>
|
||||
<string name="error_timed_out">Tempo elĉerpita</string>
|
||||
<string name="error_clocks_not_match">Aparataj horloĝoj estas malsinkronaj</string>
|
||||
<string name="error_canceled_by_user">Nuligite de uzanto</string>
|
||||
<string name="error_canceled_by_other_peer">Nuligite de alia kunulo</string>
|
||||
<string name="encryption_info_title">Ĉifrada Informo</string>
|
||||
@@ -192,11 +193,13 @@
|
||||
<string name="share_to">Kunhavigi al…</string>
|
||||
<string name="unreachable_device">%s (Neatingebla)</string>
|
||||
<string name="unreachable_device_url_share_text">URL-oj kundividitaj al neatingebla aparato estos liveritaj al ĝi post kiam ĝi fariĝos atingebla.\n\n</string>
|
||||
<string name="protocol_version">Protokolversio:</string>
|
||||
<string name="protocol_version_newer">Ĉi tiu aparato uzas pli novan protokolversion</string>
|
||||
<string name="plugin_settings_with_name">%s agordoj</string>
|
||||
<string name="invalid_device_name">Nevalida aparato nomo</string>
|
||||
<string name="shareplugin_text_saved">Ricevita teksto, konservita en tondujo</string>
|
||||
<string name="custom_devices_settings">Propra aparato listo</string>
|
||||
<string name="custom_devices_settings_summary">%d aparatoj aldoniĝis permane</string>
|
||||
<string name="custom_device_list">Aldoni aparatojn per IP</string>
|
||||
<string name="custom_device_deleted">Propra aparato forigita</string>
|
||||
<string name="custom_device_list_help">Se via aparato ne estas aŭtomate detektita, vi povas aldoni ĝian IP-adreson aŭ gastigan nomon alklakante la Ŝveban Ago-Butonon</string>
|
||||
@@ -326,6 +329,7 @@
|
||||
<string name="empty_trusted_networks_list_text">Vi ankoraŭ ne aldonis neniun fidindan reton</string>
|
||||
<string name="allow_all_networks_text">Permesi ĉion</string>
|
||||
<string name="location_permission_needed_title">Permeso bezonata</string>
|
||||
<string name="bluetooth_permission_needed_desc">KDE Connect bezonas permeson por konekti al proksimaj aparatoj por fari aparatojn parigitaj per Bluetooth disponebla en KDE Connect.</string>
|
||||
<string name="location_permission_needed_desc">KDE Connect bezonas la fonlokan permeson por scii la WiFi-reton al kiu vi estas konektita eĉ kiam la programo estas en la fono. Ĉi tio estas ĉar la nomo de la WiFi-retoj ĉirkaŭ vi povus esti uzata por trovi vian lokon, eĉ kiam tion ne faras KDE Connect.</string>
|
||||
<string name="clipboard_android_x_incompat">Android 10 forigis aliron al tondujo al ĉiuj aplikaĵoj. Ĉi tiu kromaĵo estos malŝaltita.</string>
|
||||
<string name="mpris_open_url">Daŭre ludi ĉi tie</string>
|
||||
@@ -389,6 +393,8 @@
|
||||
<string name="send_compose">Sendi</string>
|
||||
<string name="compose_send_title">Verki sendon</string>
|
||||
<string name="open_compose_send">Verki tekston</string>
|
||||
<string name="double_tap_to_drag">Duoble frapetu por treni</string>
|
||||
<string name="hold_to_drag">Teni por treni</string>
|
||||
<string name="about_kde_about"><h1>Pri</h1> <p>KDE estas tutmonda komunumo de softvar-inĝenieroj, artistoj, verkistoj, tradukistoj kaj kreintoj kiuj engaĝiĝas al <a href=https://www.gnu.org/philosophy/free -sw.html>Disvolvado de Libera Programaro</a>. KDE produktas la Plasma labortablan medion, centojn da aplikaĵoj, kaj la multajn programarajn bibliotekojn kiuj subtenas ilin.</p> </p>KDE estas kunlabora entrepreno: neniu unuopa ento stiras ĝian direkton aŭ produktojn. Anstataŭe, ni kunlaboras por atingi la komunan celon konstrui la plej bonan Liberan Programaron de la mondo. Ĉiuj bonvenas <a href=https://community.kde.org/Get_Involved>aliiĝi kaj kontribui</a> al KDE, inkluzive de vi.</p> Vizitu <a href=https://www.kde.org/>https://www.kde.org/</a> por pliaj informoj pri la KDE-komunumo kaj la programaro, kiun ni produktas.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Raporti Cimojn aŭ Dezirojn</h1> <p>Softvaro ĉiam povas esti plibonigita kaj la KDE-teamo pretas fari tion. Tamen, vi - la uzanto - devas diri al ni se io ne funkcias kiel atendite aŭ povus esti farata pli bone.</p> <p>KDE havas cimraportan sistemon. Vizitu <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> aŭ uzu la butonon \"Raporti Cimon\" el la Pri-ekrano por raporti cimojn.</p> Se vi havas sugeston por plibonigo, vi estas bonvena uzi la cimtrakan sistemon pro registri vian deziron. Certigu, ke vi uzas la severecon nomita \"Wishlist\".</string>
|
||||
<string name="about_kde_join_kde"><h1>Kuniĝu al KDE</h1> <p>Vi ne devas esti programisto por esti membro de la teamo KDE. Vi povas aliĝi al la lingvoteamoj kiuj tradukas program-interfacojn. Vi povas provizi grafikaĵojn, etosojn, sonojn, kaj plibonigitan dokumentadon. Vi decidas!</p> <p>Vizitu <a href=https://community.kde.org/Get_Involved>https://community.kde.org/Get_Involved</a> por informo pri iuj projektoj en kiuj vi povas partopreni.</p> Se vi bezonas plian informon aŭ dorkmentadon, vizito al <a href=https://techbase.kde.org/>https://techbase.kde.org/</a> provizos al vi kion vi bezonas.</string>
|
||||
@@ -403,11 +409,13 @@
|
||||
<string name="maxim_leshchenko_task">UI-plibonigoj kaj ĉi tiu pri paĝo</string>
|
||||
<string name="holger_kaelberer_task">Fora klavara kromaĵo kaj korektoj de cimoj</string>
|
||||
<string name="saikrishna_arcot_task">Subteno por uzi la klavaron en la fora eniga kromaĵo, korektoj de cimoj kaj ĝeneralaj plibonigoj</string>
|
||||
<string name="shellwen_chen_task">Plibonigi la sekurecon de SFTP, plibonigi la funkciteneblon de ĉi projekto, cimkorektoj kaj ĝeneralaj plibonigoj</string>
|
||||
<string name="everyone_else">Ĉiuj aliaj, kiuj kontribuis al KDE Connect tra la jaroj</string>
|
||||
<string name="send_clipboard">Sendi tondujon</string>
|
||||
<string name="tap_to_execute">Frapi por plenumi</string>
|
||||
<string name="plugin_stats">Statistiko de kromprogramoj</string>
|
||||
<string name="enable_udp_broadcast">Ebligi UDP-aparatan malkovron</string>
|
||||
<string name="enable_bluetooth">Ŝalti bluetooth (beta)</string>
|
||||
<string name="receive_notifications_permission_explanation">Sciigoj devas esti permesitaj ricevi ilin de aliaj aparatoj</string>
|
||||
<string name="findmyphone_notifications_explanation">La sciiga permeso estas necesa por ke la telefono povu sonori kiam la app estas en la fono</string>
|
||||
<string name="no_notifications">Sciigoj estas malŝaltitaj, vi ne ricevos alvenantajn parajn sciigojn.</string>
|
||||
@@ -415,4 +423,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Daŭrigi ludadon</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Montri silentan sciigon por daŭrigi ludadon en ĉi tiu aparato post fermo de datumportilo</string>
|
||||
<string name="notification_channel_keepwatching">Daŭrigi ludadon</string>
|
||||
<string name="ping_result">Eĥosondis en %1$d milisekundoj</string>
|
||||
<string name="ping_failed">Ne eblis eĥosondi aparaton</string>
|
||||
<string name="ping_in_progress">Eĥosondante…</string>
|
||||
<string name="device_host_invalid">Gastiganto estas nevalida. Uzu validan gastigantnomon, IPv4, aŭ IPv6</string>
|
||||
<string name="device_host_duplicate">Gastiganto jam ekzistas en la listo</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">No se encuentra el dispositivo</string>
|
||||
<string name="error_already_paired">Dispositivo ya vinculado</string>
|
||||
<string name="error_timed_out">Se ha agotado el tiempo</string>
|
||||
<string name="error_clocks_not_match">Los relojes de los dispositivos no están sincronizados</string>
|
||||
<string name="error_canceled_by_user">Cancelado por el usuario</string>
|
||||
<string name="error_canceled_by_other_peer">Cancelado por la otra parte</string>
|
||||
<string name="encryption_info_title">Información de cifrado</string>
|
||||
@@ -192,11 +193,13 @@
|
||||
<string name="share_to">Compartir con...</string>
|
||||
<string name="unreachable_device">%s (no accesible)</string>
|
||||
<string name="unreachable_device_url_share_text">Las URLs compartidas con dispositivos no accesibles se entregarán una vez que vuelvan a estar accesibles.\n\n</string>
|
||||
<string name="protocol_version">Versión del protocolo:</string>
|
||||
<string name="protocol_version_newer">Este dispositivo usa una versión más reciente del protocolo</string>
|
||||
<string name="plugin_settings_with_name">Preferencias de %s</string>
|
||||
<string name="invalid_device_name">Nombre de dispositivo no válido</string>
|
||||
<string name="shareplugin_text_saved">Texto recibido y guardado en el portapapeles</string>
|
||||
<string name="custom_devices_settings">Lista de dispositivos personalizada</string>
|
||||
<string name="custom_devices_settings_summary">%d dispositivos añadidos manualmente</string>
|
||||
<string name="custom_device_list">Añadir dispositivos por IP</string>
|
||||
<string name="custom_device_deleted">Dispositivo personalizado borrado</string>
|
||||
<string name="custom_device_list_help">Si su dispositivo no es detectado automáticamente puede añadir su dirección IP o nombre pulsando el botón de acción flotante</string>
|
||||
@@ -420,4 +423,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Continuar reproduciendo</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Mostrar una notificación silenciosa para continuar reproduciendo en este dispositivo tras cerrar el reproductor.</string>
|
||||
<string name="notification_channel_keepwatching">Continuar reproduciendo</string>
|
||||
<string name="ping_result">Conectado en %1$d mili-segundos</string>
|
||||
<string name="ping_failed">No se pudo contactar con el dispositivo</string>
|
||||
<string name="ping_in_progress">Conectando</string>
|
||||
<string name="device_host_invalid">El servidor es inválido. Use un nombre válido de servidor, IPv4 o IPv6</string>
|
||||
<string name="device_host_duplicate">El servidor ya existe en la lista</string>
|
||||
</resources>
|
||||
|
@@ -197,6 +197,7 @@
|
||||
<string name="invalid_device_name">Gailuaren izen baliogabea</string>
|
||||
<string name="shareplugin_text_saved">Testua jaso da, arbelean kopiatu da</string>
|
||||
<string name="custom_devices_settings">Gailuen zerrenda pertsonalizatua</string>
|
||||
<string name="custom_devices_settings_summary">%d gailua eskuz gehitu dira</string>
|
||||
<string name="custom_device_list">Gehitu gailuak IP bidez</string>
|
||||
<string name="custom_device_deleted">Norberak finkatutako gailua ezabatu da</string>
|
||||
<string name="custom_device_list_help">Zure gailua ez bada automatikoki detektatzen bere IP helbidea edo ostalari-izena gehitu dezakezu ekintza botoi mugikorrean klik eginez</string>
|
||||
@@ -420,4 +421,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Jarraitu jotzen</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Hedabidea itxi ondoren, gailu honetan jotzen jarraitzeko jakinarazpen ixil bat erakutsi.</string>
|
||||
<string name="notification_channel_keepwatching">Jarraitu jotzen</string>
|
||||
<string name="ping_result">«Ping» %1$d milisegundotan egin da</string>
|
||||
<string name="ping_failed">Ezin izan dio gailuari «ping» egin</string>
|
||||
<string name="ping_in_progress">«Ping» egiten...</string>
|
||||
<string name="device_host_invalid">Ostalaria baliogabea da. Erabili balio duen ostalari-izen bat, IPv4, edo IPv6</string>
|
||||
<string name="device_host_duplicate">Ostalaria jada zerrendan dago</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Laite tavoittamattomissa</string>
|
||||
<string name="error_already_paired">Laite on jo kytketty pariksi</string>
|
||||
<string name="error_timed_out">Aikakatkaisu</string>
|
||||
<string name="error_clocks_not_match">Laitteiden kelloja ei ole tahdistettu</string>
|
||||
<string name="error_canceled_by_user">Käyttäjä perui</string>
|
||||
<string name="error_canceled_by_other_peer">Vertaiskäyttäjä perui</string>
|
||||
<string name="encryption_info_title">Salaustiedot</string>
|
||||
@@ -192,11 +193,13 @@
|
||||
<string name="share_to">Jaa…</string>
|
||||
<string name="unreachable_device">%s (tavoittamattomissa)</string>
|
||||
<string name="unreachable_device_url_share_text">Tavoittamattomissa olevalle laitteelle jaetut verkko-osoitteet välitetään heti kun laite tavoitetaan.\n\n</string>
|
||||
<string name="protocol_version">Yhteyskäytäntöversio:</string>
|
||||
<string name="protocol_version_newer">Laite käyttää uudempaa yhteyskäytäntöversiota</string>
|
||||
<string name="plugin_settings_with_name">%s-asetukset</string>
|
||||
<string name="invalid_device_name">Virheellinen laitenimi</string>
|
||||
<string name="shareplugin_text_saved">Vastaanotettiin tekstiä, tallennettiin leikepöydälle</string>
|
||||
<string name="custom_devices_settings">Omien laitteiden luettelo</string>
|
||||
<string name="custom_devices_settings_summary">Käyttäjä lisäsi %d laitetta</string>
|
||||
<string name="custom_device_list">Lisää laitteita IP:llä</string>
|
||||
<string name="custom_device_deleted">Poistettiin mukautettu laite</string>
|
||||
<string name="custom_device_list_help">Ellei laitetta tunnisteta automaattisesti, sen IP-osoitteen tai konenimen voi lisätä napsauttamalla kelluvaa toimintopainiketta</string>
|
||||
@@ -420,4 +423,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Jatka toistoa</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Näytä hiljainen ilmoitus toiston jatkamisesta laitteella median sulkeuduttua</string>
|
||||
<string name="notification_channel_keepwatching">Jatka toistoa</string>
|
||||
<string name="ping_result">Pingattiin %1$d millisekunnissa</string>
|
||||
<string name="ping_failed">Laitetta ei voitu pingata</string>
|
||||
<string name="ping_in_progress">Pingataan…</string>
|
||||
<string name="device_host_invalid">Konenimi on virheellinen. Käytä kelvollista konenimeä tai IPv4- tai IPv6-osoitetta.</string>
|
||||
<string name="device_host_duplicate">Konenimi löytyy jo luettelosta</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Périphérique inaccessible</string>
|
||||
<string name="error_already_paired">Périphérique déjà associé</string>
|
||||
<string name="error_timed_out">Délai expiré</string>
|
||||
<string name="error_clocks_not_match">Les horloges du périphérique ne sont pas synchronisées.</string>
|
||||
<string name="error_canceled_by_user">Annulé par l\'utilisateur</string>
|
||||
<string name="error_canceled_by_other_peer">Annulé par un autre homologue</string>
|
||||
<string name="encryption_info_title">Informations de chiffrement</string>
|
||||
@@ -164,9 +165,11 @@
|
||||
<string name="middle_click">Envoyer un clic central</string>
|
||||
<string name="show_keyboard">Afficher le clavier</string>
|
||||
<string name="device_not_paired">Périphérique non associé</string>
|
||||
<string name="pairing_duplicate_names">Attention : Il existe plusieurs périphériques portant le même nom.</string>
|
||||
<string name="request_pairing">Demande d\'association</string>
|
||||
<string name="pairing_accept">Accepter</string>
|
||||
<string name="pairing_reject">Rejeter</string>
|
||||
<string name="pairing_explanation">L\'association de deux périphériques leur donnera un accès réciproque. Veuillez n\'associer que vos propres périphériques.</string>
|
||||
<string name="settings">Configuration</string>
|
||||
<string name="mpris_play">Lire</string>
|
||||
<string name="mpris_pause">Pause</string>
|
||||
@@ -191,12 +194,15 @@
|
||||
<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_dynamic_shortcut">%s (✕)</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">Version de protocole :</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>
|
||||
<string name="shareplugin_text_saved">Texte reçu et enregistré dans le presse-papiers</string>
|
||||
<string name="custom_devices_settings">Liste personnalisée de périphériques</string>
|
||||
<string name="custom_devices_settings_summary">%d périphériques ajoutés de façon manuelle</string>
|
||||
<string name="custom_device_list">Ajouter des périphériques par IP</string>
|
||||
<string name="custom_device_deleted">Périphérique personnalisé supprimé</string>
|
||||
<string name="custom_device_list_help">Si votre périphérique n\'est pas détecté automatiquement, vous pouvez ajouter son adresse IP ou son nom d\'hôte en cliquant sur le bouton d\'action flottant</string>
|
||||
@@ -306,6 +312,8 @@
|
||||
<string name="devices">Périphériques</string>
|
||||
<string name="settings_rename">Nom du périphérique</string>
|
||||
<string name="settings_dark_mode">Thème sombre</string>
|
||||
<string name="settings_export_logs">Exporter les journaux de KDEConnect</string>
|
||||
<string name="settings_export_logs_text">Générez un fichier avec des informations d\'exécution pouvant aider à résoudre les problèmes.</string>
|
||||
<string name="settings_more_settings_title">Plus de paramètres</string>
|
||||
<string name="settings_more_settings_text">Les paramètres par appareil sont disponibles dans la rubrique « Paramètres des modules externes » sur l\'appareil.</string>
|
||||
<string name="setting_persistent_notification">Afficher une notification persistante</string>
|
||||
@@ -420,4 +428,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Continuer la lecture</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Afficher une notification silencieuse pour continuer à jouer sur ce périphérique après la fermeture du média.</string>
|
||||
<string name="notification_channel_keepwatching">Continuer la lecture</string>
|
||||
<string name="ping_result">Interrogé en %1$d millisecondes</string>
|
||||
<string name="ping_failed">Il est impossible d\'interroger un périphérique (Par ping)</string>
|
||||
<string name="ping_in_progress">Interrogation par ping en cours...</string>
|
||||
<string name="device_host_invalid">L\'hôte est non valable. Veuillez utiliser un nom d\'hôte valable, IPv4 ou IPv6</string>
|
||||
<string name="device_host_duplicate">L\'hôte existe déjà dans la liste.</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">O dispositivo está inaccesíbel.</string>
|
||||
<string name="error_already_paired">O dispositivo xa está emparellado.</string>
|
||||
<string name="error_timed_out">Esgotouse o tempo límite</string>
|
||||
<string name="error_clocks_not_match">Os reloxos do dispositivos non están sincronizados.</string>
|
||||
<string name="error_canceled_by_user">Cancelouno a persoa usuaria.</string>
|
||||
<string name="error_canceled_by_other_peer">Cancelouse remotamente</string>
|
||||
<string name="encryption_info_title">Información do cifrado</string>
|
||||
@@ -192,11 +193,13 @@
|
||||
<string name="share_to">Compartir con…</string>
|
||||
<string name="unreachable_device">%s (inaccesíbel)</string>
|
||||
<string name="unreachable_device_url_share_text">Os URL que se compartan cun dispositivo inaccesíbel entregaranse cando o dispositivo estea accesíbel.\n\n</string>
|
||||
<string name="protocol_version">Versión do protocolo:</string>
|
||||
<string name="protocol_version_newer">Este dispositivo usa unha versión máis nova do protocolo.</string>
|
||||
<string name="plugin_settings_with_name">Configuración de %s</string>
|
||||
<string name="invalid_device_name">Nome de dispositivo incorrecto</string>
|
||||
<string name="shareplugin_text_saved">Recibiuse un texto e gardouse no portapapeis</string>
|
||||
<string name="custom_devices_settings">Lista de dispositivos personalizada</string>
|
||||
<string name="custom_devices_settings_summary">%d dispositivos engadidos manualmente</string>
|
||||
<string name="custom_device_list">Engadir dispositivos por IP</string>
|
||||
<string name="custom_device_deleted">Eliminouse o dispositivo personalizado</string>
|
||||
<string name="custom_device_list_help">Se o seu dispositivo non se detecta automaticamente pode engadir o seu enderezo IP ou nome de máquina premendo o botón flotante de acción</string>
|
||||
@@ -420,4 +423,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Continuar reproducindo</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Amosar unha notificación silenciosa para continuar reproducindo neste dispositivo tras pechar o contido multimedia.</string>
|
||||
<string name="notification_channel_keepwatching">Continuar reproducindo</string>
|
||||
<string name="ping_result">Enviouse un ping en %1$d milisegundos</string>
|
||||
<string name="ping_failed">Non foi posíbel enviar un ping ao dispositivo.</string>
|
||||
<string name="ping_in_progress">Enviando un ping…</string>
|
||||
<string name="device_host_invalid">O servidor non é válido. Use un nome de servidor, enderezo IPv4 ou enderezo IPv6 válido.</string>
|
||||
<string name="device_host_duplicate">O servidor xa existe na lista.</string>
|
||||
</resources>
|
||||
|
@@ -185,11 +185,14 @@
|
||||
<string name="mpris_notification_settings_summary">Gerir kleift að stýra margmiðlunarspilurunum þínum án þess að opna KDE-tengingar</string>
|
||||
<string name="share_to">Deila með…</string>
|
||||
<string name="unreachable_device">%s (ekki aðgengilegt)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="protocol_version">Útgáfa samskiptamáta:</string>
|
||||
<string name="protocol_version_newer">Þetta tæki notar nýrri útgáfu samskiptastaðals</string>
|
||||
<string name="plugin_settings_with_name">Stillingar %s</string>
|
||||
<string name="invalid_device_name">Ógilt heiti tækis</string>
|
||||
<string name="shareplugin_text_saved">Tók við texta, vistaði á klippispjald</string>
|
||||
<string name="custom_devices_settings">Listi yfir sérsniðin tæki</string>
|
||||
<string name="custom_devices_settings_summary">%d tækjum bætt við handvirkt</string>
|
||||
<string name="custom_device_list">Bæta við tækjum eftir auðkennum</string>
|
||||
<string name="custom_device_deleted">Sérsniðnu tæki eytt</string>
|
||||
<string name="custom_device_fab_hint">Bæta við tæki</string>
|
||||
@@ -279,6 +282,7 @@
|
||||
<string name="devices">Tæki</string>
|
||||
<string name="settings_rename">Heiti tækis</string>
|
||||
<string name="settings_dark_mode">Dökkt þema</string>
|
||||
<string name="settings_export_logs">Flytja út atvikaskrár KDE Connect</string>
|
||||
<string name="settings_more_settings_title">Fleiri stillingar</string>
|
||||
<string name="setting_persistent_notification">Birta viðvarandi tilkynningu</string>
|
||||
<string name="setting_persistent_notification_oreo">Viðvarandi tilkynning</string>
|
||||
@@ -380,4 +384,7 @@
|
||||
<string name="mpris_keepwatching">Halda áfram spilun</string>
|
||||
<string name="mpris_keepwatching_settings_title">Halda áfram spilun</string>
|
||||
<string name="notification_channel_keepwatching">Halda áfram spilun</string>
|
||||
<string name="ping_failed">Gat ekki pikkað í tæki</string>
|
||||
<string name="device_host_invalid">Ógilt vélarheiti. Notaðu gilt vélarheiti, IPv4 eða IPv6</string>
|
||||
<string name="device_host_duplicate">Hýsivélin er nú þegar á listanum</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Dispositivo fuori portata</string>
|
||||
<string name="error_already_paired">Dispositivo già associato</string>
|
||||
<string name="error_timed_out">Richiesta scaduta</string>
|
||||
<string name="error_clocks_not_match">Gli orologi dei dispositivi non sono sincronizzati</string>
|
||||
<string name="error_canceled_by_user">Annullata dall\'utente</string>
|
||||
<string name="error_canceled_by_other_peer">Annullata dal dispositivo remoto</string>
|
||||
<string name="encryption_info_title">Informazioni di cifratura</string>
|
||||
@@ -164,9 +165,11 @@
|
||||
<string name="middle_click">Invia clic tasto centrale</string>
|
||||
<string name="show_keyboard">Mostra tastiera</string>
|
||||
<string name="device_not_paired">Dispositivo non associato</string>
|
||||
<string name="pairing_duplicate_names">Attenzione: ci sono più dispositivi con lo stesso nome.</string>
|
||||
<string name="request_pairing">Richiedi associazione</string>
|
||||
<string name="pairing_accept">Accetta</string>
|
||||
<string name="pairing_reject">Rifiuta</string>
|
||||
<string name="pairing_explanation">L\'associazione di due dispositivi consentirà loro l\'accesso reciproco. Associa solo i tuoi dispositivi.</string>
|
||||
<string name="settings">Impostazioni</string>
|
||||
<string name="mpris_play">Riproduci</string>
|
||||
<string name="mpris_pause">Pausa</string>
|
||||
@@ -191,12 +194,15 @@
|
||||
<string name="mpris_notification_settings_summary">Consenti di controllare i lettori multimediali senza aprire KDE Connect</string>
|
||||
<string name="share_to">Condividi con…</string>
|
||||
<string name="unreachable_device">%s (non raggiungibile)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="unreachable_device_url_share_text">Gli URL condivisi su un dispositivo irraggiungibile saranno recapitati una volta che sarà tornato raggiungibile.\n\n</string>
|
||||
<string name="protocol_version">Versione del protocollo:</string>
|
||||
<string name="protocol_version_newer">Questo dispositivo usa una nuova versione del protocollo di rete</string>
|
||||
<string name="plugin_settings_with_name">Impostazioni di %s</string>
|
||||
<string name="invalid_device_name">Nome dispositivo non valido</string>
|
||||
<string name="shareplugin_text_saved">Testo ricevuto, salvato negli appunti</string>
|
||||
<string name="custom_devices_settings">Elenco dispositivi personalizzati</string>
|
||||
<string name="custom_devices_settings_summary">%d dispositivi aggiunti manualmente</string>
|
||||
<string name="custom_device_list">Aggiungi dispositivi per IP</string>
|
||||
<string name="custom_device_deleted">Dispositivo personalizzato eliminato</string>
|
||||
<string name="custom_device_list_help">Se il tuo dispositivo non è rilevato automaticamente, puoi aggiungere il suo indirizzo IP o il nome host facendo clic sul pulsante Azione</string>
|
||||
@@ -306,6 +312,8 @@
|
||||
<string name="devices">Dispositivi</string>
|
||||
<string name="settings_rename">Nome dispositivo</string>
|
||||
<string name="settings_dark_mode">Tema scuro</string>
|
||||
<string name="settings_export_logs">Esporta i registri di KDE Connect</string>
|
||||
<string name="settings_export_logs_text">Genera un file con informazioni di esecuzione che possono aiutare a risolvere i problemi.</string>
|
||||
<string name="settings_more_settings_title">Altre impostazioni</string>
|
||||
<string name="settings_more_settings_text">Le impostazioni per dispositivo sono disponibili sotto «Impostazioni estensioni» dall\'interno del dispositivo.</string>
|
||||
<string name="setting_persistent_notification">Mostra notifica persistente</string>
|
||||
@@ -420,4 +428,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Continua la riproduzione</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Mostra una notifica silenziosa per continuare a giocare su questo dispositivo dopo aver chiuso il supporto multimediale</string>
|
||||
<string name="notification_channel_keepwatching">Continua la riproduzione</string>
|
||||
<string name="ping_result">Ping effettuato in %1$d millisecondi</string>
|
||||
<string name="ping_failed">Impossibile effettuare il ping del dispositivo</string>
|
||||
<string name="ping_in_progress">Ping in corso…</string>
|
||||
<string name="device_host_invalid">L\'host non è valido. Utilizza un nome host valido, IPv4 o IPv6</string>
|
||||
<string name="device_host_duplicate">L\'host esiste già nell\'elenco</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">המכשיר לא זמין</string>
|
||||
<string name="error_already_paired">המכשיר כבר מצומד</string>
|
||||
<string name="error_timed_out">נגמר הזמן</string>
|
||||
<string name="error_clocks_not_match">שעוני המכשירים לא תואמים</string>
|
||||
<string name="error_canceled_by_user">בוטל על ידי המשתמש</string>
|
||||
<string name="error_canceled_by_other_peer">בוטל מהצד השני</string>
|
||||
<string name="encryption_info_title">פרטי הצפנה</string>
|
||||
@@ -180,9 +181,11 @@
|
||||
<string name="middle_click">שליחת לחיצה אמצעית (גלגלת)</string>
|
||||
<string name="show_keyboard">הצגת מקלדת</string>
|
||||
<string name="device_not_paired">המכשיר לא מצומד</string>
|
||||
<string name="pairing_duplicate_names">אזהרה: יש כמה מכשירים שונים באותו השם.</string>
|
||||
<string name="request_pairing">בקשת צימוד</string>
|
||||
<string name="pairing_accept">אישור</string>
|
||||
<string name="pairing_reject">דחייה</string>
|
||||
<string name="pairing_explanation">צימוד שני מכשירים יעניק להם גישה מאחד לשני ולהפך. יש לצמד רק אם המכשירים הם שלך.</string>
|
||||
<string name="settings">הגדרות</string>
|
||||
<string name="mpris_play">נגינה</string>
|
||||
<string name="mpris_pause">השהיה</string>
|
||||
@@ -207,12 +210,15 @@
|
||||
<string name="mpris_notification_settings_summary">לאפשר שליטה בנגני המדיה שלך מבלי לפתוח את KDE Connect</string>
|
||||
<string name="share_to">שיתוף אל…</string>
|
||||
<string name="unreachable_device">%s (לא נגיש)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="unreachable_device_url_share_text">כתובות שותפו להתקן בלתי נגיש והן תועברנה אליו ברגע שישוב להיות נגיש.\n\n</string>
|
||||
<string name="protocol_version">גרסת פרוטוקול:</string>
|
||||
<string name="protocol_version_newer">המכשיר משתמש בגרסה חדשה יותר</string>
|
||||
<string name="plugin_settings_with_name">הגדרות %s</string>
|
||||
<string name="invalid_device_name">שם המכשיר שגוי</string>
|
||||
<string name="shareplugin_text_saved">התקבל טקסט, נשמר ללוח הגזירים</string>
|
||||
<string name="custom_devices_settings">רשימת מכשירים מותאמת אישית</string>
|
||||
<string name="custom_devices_settings_summary">%d התקנים נוספו ידנית</string>
|
||||
<string name="custom_device_list">הוספת מכשירים לפי IP</string>
|
||||
<string name="custom_device_deleted">מכשיר מותאם אישית נמחק</string>
|
||||
<string name="custom_device_list_help">אם המכשיר שלך לא מזוהה אוטומטית אפשר להוסיף את כתובת ה־IP או את שם המארח שלו בלחיצה על כפתור הפעולה הצף</string>
|
||||
@@ -252,7 +258,7 @@
|
||||
<string name="pairing_description">מכשירים אחרים שמריצים KDE Connect ברשת הנוכחית צריכים להופיע פה.</string>
|
||||
<string name="device_rename_title">שינוי שם מכשיר</string>
|
||||
<string name="device_rename_confirm">שינוי שם</string>
|
||||
<string name="refresh">רענון</string>
|
||||
<string name="refresh">ריענון</string>
|
||||
<string name="unreachable_description">ההתקן המצומד לא זמין, כדאי לוודא שהוא מחובר לאותה רשת כמוך.</string>
|
||||
<string name="no_wifi">לא התחברת לאף רשת אלחוטית, לכן אין לך אפשרות לראות מכשירים כלשהם. לחיצה כאן תפעיל את הרשת האלחוטית.</string>
|
||||
<string name="on_non_trusted_message">לא רשת מהימנה: גילוי אוטומטי מושבת.</string>
|
||||
@@ -322,6 +328,8 @@
|
||||
<string name="devices">מכשירים</string>
|
||||
<string name="settings_rename">שם המכשיר</string>
|
||||
<string name="settings_dark_mode">ערכת עיצוב כהה</string>
|
||||
<string name="settings_export_logs">ייצוא היומנים של KDE Connect</string>
|
||||
<string name="settings_export_logs_text">ליצור קובץ עם פרטי הפעלה שיכול לסייע בפתרון תקלות.</string>
|
||||
<string name="settings_more_settings_title">הגדרות נוספות</string>
|
||||
<string name="settings_more_settings_text">אפשר למצוא הגדרות נקודתיות למכשיר תחת ‚הגדרות תוסף’ מתוך המכשיר.</string>
|
||||
<string name="setting_persistent_notification">הצגת התראה קבועה</string>
|
||||
@@ -436,4 +444,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">להמשיך לנגן</string>
|
||||
<string name="mpris_keepwatching_settings_summary">הצגת התראה שקטה כדי להמשיך לנגן בהתקן הזה לאחר סגירת המדיה</string>
|
||||
<string name="notification_channel_keepwatching">להמשיך לנגן</string>
|
||||
<string name="ping_result">הפינג ארך %1$d מילישניות</string>
|
||||
<string name="ping_failed">לא ניתן לשלוח פינג להתקן</string>
|
||||
<string name="ping_in_progress">נשלח פינג…</string>
|
||||
<string name="device_host_invalid">המארח שגוי. נא להשתמש בשם מארח, IPv4 או IPv6 תקניים.</string>
|
||||
<string name="device_host_duplicate">המארח כבר קיים ברשימה</string>
|
||||
</resources>
|
||||
|
@@ -89,15 +89,12 @@
|
||||
<string name="error_not_reachable">მოწყობილობა მიუწვდოელია</string>
|
||||
<string name="error_already_paired">მოწყობილობა უკვე დაწყვილებულია</string>
|
||||
<string name="error_timed_out">ვადა გავიდა</string>
|
||||
<string name="error_clocks_not_match">მოწყობილობის საათების სინქრონიზაცია დარღვეულია</string>
|
||||
<string name="error_canceled_by_user">გაუქმებულია მომხმარებლის მიერ</string>
|
||||
<string name="error_canceled_by_other_peer">გაუქმებულია პარტნიორის მიერ</string>
|
||||
<string name="encryption_info_title">დაშიფვრის ინფორმაცია</string>
|
||||
<string name="pair_requested">დაწყვილების მოთხოვნა</string>
|
||||
<string name="pair_succeeded">დაწყვილება წარმატებულია</string>
|
||||
<plurals name="incoming_files_text">
|
||||
<item quantity="one">File: %1s</item>
|
||||
<item quantity="other">(File %2$d of %3$d) : %1$s</item>
|
||||
</plurals>
|
||||
<plurals name="outgoing_files_text">
|
||||
<item quantity="one">ფაილი: %1$s</item>
|
||||
<item quantity="other">(ფაილი %2$d of %3$d) : %1$s</item>
|
||||
@@ -134,6 +131,7 @@
|
||||
</string-array>
|
||||
<string name="share_to">გაზიარება…</string>
|
||||
<string name="unreachable_device">%s (მიუწვდომელია)</string>
|
||||
<string name="protocol_version">პროტოკოლის ვერსია:</string>
|
||||
<string name="protocol_version_newer">მოწყობილობა პროტოკოლის უფრო ახალ ვერსიას იყენებს</string>
|
||||
<string name="plugin_settings_with_name">%s-ის მორგება</string>
|
||||
<string name="invalid_device_name">მოწყობილობის არასწორი სახელი</string>
|
||||
@@ -217,6 +215,7 @@
|
||||
<string name="devices">მოწყობილობები</string>
|
||||
<string name="settings_rename">მოწყობილობის სახელი</string>
|
||||
<string name="settings_dark_mode">ბნელი თემა</string>
|
||||
<string name="settings_export_logs">KDE Connect-ის ჟურნალის გატანა</string>
|
||||
<string name="settings_more_settings_title">მეტი პარამეტრი</string>
|
||||
<string name="extra_options">დამატებითი პარამეტრები</string>
|
||||
<string name="privacy_options">კონფიდენციალობის პარამეტრები</string>
|
||||
@@ -292,4 +291,5 @@
|
||||
<string name="send_clipboard">ბუფერის გაგზავნა</string>
|
||||
<string name="tap_to_execute">ქმედების შესასრულებლად დაატყაპუნეთ</string>
|
||||
<string name="plugin_stats">მოდულის სტატისტიკა</string>
|
||||
<string name="ping_failed">მოწყობილობა არ იპინგება</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">장치에 접근할 수 없음</string>
|
||||
<string name="error_already_paired">장치가 이미 페어링됨</string>
|
||||
<string name="error_timed_out">시간 초과됨</string>
|
||||
<string name="error_clocks_not_match">장치 시계가 맞지 않음</string>
|
||||
<string name="error_canceled_by_user">사용자가 취소함</string>
|
||||
<string name="error_canceled_by_other_peer">다른 쪽에서 취소함</string>
|
||||
<string name="encryption_info_title">암호화 정보</string>
|
||||
@@ -156,9 +157,11 @@
|
||||
<string name="middle_click">가운데 단추 클릭 신호 보내기</string>
|
||||
<string name="show_keyboard">키보드 표시</string>
|
||||
<string name="device_not_paired">장치가 페어링되지 않음</string>
|
||||
<string name="pairing_duplicate_names">경고: 이름이 같은 장치가 여러 대 있습니다.</string>
|
||||
<string name="request_pairing">페어링 요청</string>
|
||||
<string name="pairing_accept">수락</string>
|
||||
<string name="pairing_reject">거부</string>
|
||||
<string name="pairing_explanation">두 장치를 페어링하면 서로간의 접근을 허용합니다. 내 장치끼리만 페어링하십시오.</string>
|
||||
<string name="settings">설정</string>
|
||||
<string name="mpris_play">재생</string>
|
||||
<string name="mpris_pause">일시 정지</string>
|
||||
@@ -183,12 +186,15 @@
|
||||
<string name="mpris_notification_settings_summary">KDE Connect를 열지 않고 미디어 재생기 제어</string>
|
||||
<string name="share_to">다음으로 공유…</string>
|
||||
<string name="unreachable_device">%s(접근할 수 없음)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s(✕)</string>
|
||||
<string name="unreachable_device_url_share_text">접근할 수 없는 장치와 공유한 URL은 장치에 다시 접근할 수 있게 될 때 전달됩니다.\n\n</string>
|
||||
<string name="protocol_version">프로토콜 버전:</string>
|
||||
<string name="protocol_version_newer">이 장치의 프로토콜 버전이 더 새롭습니다</string>
|
||||
<string name="plugin_settings_with_name">%s 설정</string>
|
||||
<string name="invalid_device_name">잘못된 장치 이름</string>
|
||||
<string name="shareplugin_text_saved">텍스트 수신, 클립보드에 복사됨</string>
|
||||
<string name="custom_devices_settings">사용자 정의 장치 목록</string>
|
||||
<string name="custom_devices_settings_summary">장치 %d개를 수동으로 추가함</string>
|
||||
<string name="custom_device_list">IP로 장치 추가</string>
|
||||
<string name="custom_device_deleted">사용자 정의 장치 삭제됨</string>
|
||||
<string name="custom_device_list_help">장치가 자동으로 감지되지 않았다면 떠 다니는 동작 단추를 클릭해서 IP 주소나 호스트 이름으로 장치를 수동으로 추가할 수 있습니다</string>
|
||||
@@ -298,6 +304,8 @@
|
||||
<string name="devices">장치</string>
|
||||
<string name="settings_rename">장치 이름</string>
|
||||
<string name="settings_dark_mode">어두운 테마</string>
|
||||
<string name="settings_export_logs">KDE Connect 로그 내보내기</string>
|
||||
<string name="settings_export_logs_text">문제 해결을 위해서 실행 정보를 포함하는 파일을 생성합니다.</string>
|
||||
<string name="settings_more_settings_title">더 많은 설정</string>
|
||||
<string name="settings_more_settings_text">장치별 설정은 각각 장치의 \'플러그인 설정\'에서 확인할 수 있습니다.</string>
|
||||
<string name="setting_persistent_notification">항상 표시되는 알림 표시</string>
|
||||
@@ -412,4 +420,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">계속 재생</string>
|
||||
<string name="mpris_keepwatching_settings_summary">미디어를 닫은 후 이 장치에서 계속 재생할 수 있는 조용한 알림 표시</string>
|
||||
<string name="notification_channel_keepwatching">계속 재생</string>
|
||||
<string name="ping_result">핑 시간: %1$d 밀리초</string>
|
||||
<string name="ping_failed">장치에 핑을 보낼 수 없음</string>
|
||||
<string name="ping_in_progress">핑 진행 중…</string>
|
||||
<string name="device_host_invalid">호스트가 잘못되었습니다. 올바른 호스트 이름, IPv4, IPv6 주소를 지정하십시오</string>
|
||||
<string name="device_host_duplicate">호스트가 목록에 이미 있음</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Apparaat niet bereikbaar</string>
|
||||
<string name="error_already_paired">Apparaat is al gepaard</string>
|
||||
<string name="error_timed_out">Tijdslimiet overschreden</string>
|
||||
<string name="error_clocks_not_match">Klokken op apparaten lopen niet gelijk</string>
|
||||
<string name="error_canceled_by_user">Geannuleerd door gebruiker</string>
|
||||
<string name="error_canceled_by_other_peer">Geannuleerd door andere kant</string>
|
||||
<string name="encryption_info_title">Versleutelde informatie</string>
|
||||
@@ -164,9 +165,11 @@
|
||||
<string name="middle_click">Verstuur een middelste muisklik</string>
|
||||
<string name="show_keyboard">Toetsenbord tonen</string>
|
||||
<string name="device_not_paired">Apparaat is niet gepaard</string>
|
||||
<string name="pairing_duplicate_names">Voorzichtig: er zijn meerdere apparaten met dezelfde naam.</string>
|
||||
<string name="request_pairing">Verzoek voor maken van paar</string>
|
||||
<string name="pairing_accept">Accepteren</string>
|
||||
<string name="pairing_reject">Afwijzen</string>
|
||||
<string name="pairing_explanation">Twee apparaten paren zal ze toegang geven tot elkaar. Paar alleen uw eigen apparaten.</string>
|
||||
<string name="settings">Instellingen</string>
|
||||
<string name="mpris_play">Afspelen</string>
|
||||
<string name="mpris_pause">Pauzeren</string>
|
||||
@@ -191,12 +194,15 @@
|
||||
<string name="mpris_notification_settings_summary">Staat besturing van uw mediaspelers toe zonder KDE Connect te openen</string>
|
||||
<string name="share_to">Delen met…</string>
|
||||
<string name="unreachable_device">%s (niet bereikbaar)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="unreachable_device_url_share_text">URL\'s gedeeld met een niet bereikbaar apparaat zullen er afgeleverd worden wanneer deze bereikbaar wordt.\n\n</string>
|
||||
<string name="protocol_version">Protocolversie:</string>
|
||||
<string name="protocol_version_newer">Dit apparaat gebruikt een nieuwere protocolversie</string>
|
||||
<string name="plugin_settings_with_name">Instellingen van %s</string>
|
||||
<string name="invalid_device_name">Ongeldige apparaatnaam</string>
|
||||
<string name="shareplugin_text_saved">Oontvangen tekst, opgeslagen op klembord</string>
|
||||
<string name="custom_devices_settings">Aangepaste lijst apparaten</string>
|
||||
<string name="custom_devices_settings_summary">%d apparaten handmatig toegevoegd</string>
|
||||
<string name="custom_device_list">Voeg apparaten toe per IP-adres</string>
|
||||
<string name="custom_device_deleted">Aangepaste apparaat verwijderd</string>
|
||||
<string name="custom_device_list_help">Als uw apparaat niet automatisch wordt gedetecteerd kunt u zijn IP-adres of hostnaam toevoegen door te klikken op de knop Zwevende actie</string>
|
||||
@@ -306,6 +312,8 @@
|
||||
<string name="devices">Apparaten</string>
|
||||
<string name="settings_rename">Apparaatnaam</string>
|
||||
<string name="settings_dark_mode">Donker thema</string>
|
||||
<string name="settings_export_logs">Logs van KDE Connect exporteren</string>
|
||||
<string name="settings_export_logs_text">Genereert een bestand met uitvoeringsinformatie die kan helpen problemen op te lossen.</string>
|
||||
<string name="settings_more_settings_title">Meer instellingen</string>
|
||||
<string name="settings_more_settings_text">Instellingen per apparaat kunnen gevonden worden onder \'Plug-in-instellingen\' vanuit een apparaat.</string>
|
||||
<string name="setting_persistent_notification">Blijvende melding tonen</string>
|
||||
@@ -420,4 +428,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Doorgaan met afspelen</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Een stille melding tonen om door te gaan met afspelen op dit apparaat na sluiten van het medium</string>
|
||||
<string name="notification_channel_keepwatching">Doorgaan met afspelen</string>
|
||||
<string name="ping_result">Ping verkregen in %1$d milliseconden</string>
|
||||
<string name="ping_failed">Kreeg geen antwoord op ping van apparaat</string>
|
||||
<string name="ping_in_progress">Ping wordt verstuurt</string>
|
||||
<string name="device_host_invalid">Host is ongeldig. Gebruik een geldige hostnaam, IPv4 of IPv6</string>
|
||||
<string name="device_host_duplicate">Host bestaat al in de lijst</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Urządzenie nieosiągalne</string>
|
||||
<string name="error_already_paired">Urządzenie już sparowano</string>
|
||||
<string name="error_timed_out">Upłynął czas na odpowiedź</string>
|
||||
<string name="error_clocks_not_match">Zegary urządzenia nie są zsynchronizowane</string>
|
||||
<string name="error_canceled_by_user">Użytkownik zaniechał</string>
|
||||
<string name="error_canceled_by_other_peer">Inny uczestnik zaniechał</string>
|
||||
<string name="encryption_info_title">Dane o szyfrowaniu</string>
|
||||
@@ -208,11 +209,13 @@
|
||||
<string name="share_to">Udostępnij urządzeniu...</string>
|
||||
<string name="unreachable_device">%s (nieosiągalne)</string>
|
||||
<string name="unreachable_device_url_share_text">Adres URL udostępniony nieosiągalnemu urządzeniu zostanie do niego dostarczony zaraz po tym jak stanie się osiągalne.\n\n</string>
|
||||
<string name="protocol_version">Wersja protokołu:</string>
|
||||
<string name="protocol_version_newer">Urządzenie to używa nowszej wersji protokołu</string>
|
||||
<string name="plugin_settings_with_name">Ustawienia %s</string>
|
||||
<string name="invalid_device_name">Nieprawidłowa nazwa urządzenia</string>
|
||||
<string name="shareplugin_text_saved">Otrzymano tekst, zapisano do schowka</string>
|
||||
<string name="custom_devices_settings">Lista własnych urządzeń</string>
|
||||
<string name="custom_devices_settings_summary">%d urządzeń dodanych ręcznie</string>
|
||||
<string name="custom_device_list">Dodaj urządzenie po adresie IP</string>
|
||||
<string name="custom_device_deleted">Usunięto własne urządzenie</string>
|
||||
<string name="custom_device_list_help">Jeśli twoje urządzenie nie zostało wykryte samoczynnie, to możesz dodać je ręcznie po wpisaniu jego adresu IP lub nazwy gospodarza. Aby to zrobić, naciśnij pomarańczowy przycisk u dołu ekranu.</string>
|
||||
@@ -436,4 +439,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Kontynuuj odtwarzanie</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Pokaż ciche powiadomienie, aby kontynuować odtwarzanie na tym urządzeniu po zamknięciu mediów</string>
|
||||
<string name="notification_channel_keepwatching">Kontynuuj odtwarzanie</string>
|
||||
<string name="ping_result">Odpowiedział w %1$d milisekund</string>
|
||||
<string name="ping_failed">Nie można wysłać pingu do urządzenia</string>
|
||||
<string name="ping_in_progress">Wysyłanie pingu…</string>
|
||||
<string name="device_host_invalid">Nazwa gospodarza jest nieprawidłowa. Użyj prawidłowej nazwy, adresu IPv4 lub IPv6</string>
|
||||
<string name="device_host_duplicate">Nazwa gospodarza już znajduje się na liście</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Naprava ni dosegljiva</string>
|
||||
<string name="error_already_paired">Naprava je že uparjena</string>
|
||||
<string name="error_timed_out">Čas je potekel</string>
|
||||
<string name="error_clocks_not_match">Ure naprave niso sinhronizirane</string>
|
||||
<string name="error_canceled_by_user">Preklical uporabnik</string>
|
||||
<string name="error_canceled_by_other_peer">Preklican od drugega vrstnika</string>
|
||||
<string name="encryption_info_title">Informacija o šifriranju</string>
|
||||
@@ -130,10 +131,10 @@
|
||||
<item quantity="other">Prejemanje %1$d datotek od %2$s</item>
|
||||
</plurals>
|
||||
<plurals name="incoming_files_text">
|
||||
<item quantity="one">(Datotek %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="one">(Datoteka %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="two">(Datoteka %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="few">(Datoteki %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="other">(Datoteke %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="few">(Datoteka %2$d od %3$d) : %1$s</item>
|
||||
<item quantity="other">(Datoteka %2$d od %3$d) : %1$s</item>
|
||||
</plurals>
|
||||
<plurals name="outgoing_file_title">
|
||||
<item quantity="one">Pošiljanje %1$d datotek na %2$s</item>
|
||||
@@ -180,9 +181,11 @@
|
||||
<string name="middle_click">Pošlji sredinski klik</string>
|
||||
<string name="show_keyboard">Pokaži tipkovnico</string>
|
||||
<string name="device_not_paired">Naprava ni uparjena</string>
|
||||
<string name="pairing_duplicate_names">POZOR: Obstaja več naprav z istim imenom.</string>
|
||||
<string name="request_pairing">Zahtevaj uparjanje</string>
|
||||
<string name="pairing_accept">Sprejmi</string>
|
||||
<string name="pairing_reject">Zavrni</string>
|
||||
<string name="pairing_explanation">Uparjanje dveh naprav jim bo omogočilo dostop ene do druge. Uparjajte samo svoje lastne naprave.</string>
|
||||
<string name="settings">Nastavitve</string>
|
||||
<string name="mpris_play">Predvajaj</string>
|
||||
<string name="mpris_pause">Premor</string>
|
||||
@@ -207,12 +210,15 @@
|
||||
<string name="mpris_notification_settings_summary">Dovoli nadzor nad predvajalniki medijev, ne da bi odprli KDE Connect</string>
|
||||
<string name="share_to">Deli z…</string>
|
||||
<string name="unreachable_device">%s (Nedosegljiva)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="unreachable_device_url_share_text">URL-ji, ki so v skupni rabi z nedosegljivo napravo, ji bodo dostavljeni, ko postane dosegljiva.\n\n</string>
|
||||
<string name="protocol_version">Protokol različice:</string>
|
||||
<string name="protocol_version_newer">Ta naprava uporablja novejšo različico protokola</string>
|
||||
<string name="plugin_settings_with_name">Nastavitve %s</string>
|
||||
<string name="invalid_device_name">Neveljavno ime naprave</string>
|
||||
<string name="shareplugin_text_saved">Prejeto besedilo shranjeno na odložišče</string>
|
||||
<string name="custom_devices_settings">Seznam naprav po meri</string>
|
||||
<string name="custom_devices_settings_summary">%d naprav, dodanih ročno</string>
|
||||
<string name="custom_device_list">Dodaj naprave po IP</string>
|
||||
<string name="custom_device_deleted">Zbrisana naprava po meri</string>
|
||||
<string name="custom_device_list_help">Če vaša naprava ni samodejno zaznana, lahko dodate njen IP naslov ali ime gostitelja s klikom na gumb Plavajoče dejanje</string>
|
||||
@@ -322,6 +328,8 @@
|
||||
<string name="devices">Naprave</string>
|
||||
<string name="settings_rename">Ime naprave</string>
|
||||
<string name="settings_dark_mode">Temna tema</string>
|
||||
<string name="settings_export_logs">Izvozi dnevnike KDE Connect</string>
|
||||
<string name="settings_export_logs_text">Ustvari datoteko z informacijami o izvajanju, ki lahko pomagajo pri odpravljanju težav.</string>
|
||||
<string name="settings_more_settings_title">Več nastavitev</string>
|
||||
<string name="settings_more_settings_text">Nastavitve posamezne naprave najdete v razdelku \'Nastavitve vtičnikov\' v napravi.</string>
|
||||
<string name="setting_persistent_notification">Prikazuj trajno obvestilo</string>
|
||||
@@ -436,4 +444,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Nadaljuj s predvajanjem</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Pokaži tiho obvestilo za nadaljevanje predvajanja v tej napravi po zaprtju predstavnosti</string>
|
||||
<string name="notification_channel_keepwatching">Nadaljuj s predvajanjem</string>
|
||||
<string name="ping_result">Ping-an v %1$d milisekundah</string>
|
||||
<string name="ping_failed">Ni bilo mogoče pingati naprave</string>
|
||||
<string name="ping_in_progress">Pinganje…</string>
|
||||
<string name="device_host_invalid">Gostitelj ni veljaven. Uporabite veljavno ime gostitelja, IPv4 ali IPv6</string>
|
||||
<string name="device_host_duplicate">Gostitelj že obstaja na seznamu</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Apparaten kan inte nås</string>
|
||||
<string name="error_already_paired">Apparat redan parkopplad</string>
|
||||
<string name="error_timed_out">Tidsgräns överskriden</string>
|
||||
<string name="error_clocks_not_match">Apparaternas klockor är inte synkroniserade</string>
|
||||
<string name="error_canceled_by_user">Avbruten av användaren</string>
|
||||
<string name="error_canceled_by_other_peer">Avbruten av motparten</string>
|
||||
<string name="encryption_info_title">Krypteringsinformation</string>
|
||||
@@ -192,11 +193,13 @@
|
||||
<string name="share_to">Dela med…</string>
|
||||
<string name="unreachable_device">%s (kan inte nås)</string>
|
||||
<string name="unreachable_device_url_share_text">Webbadress som delas med en apparat som inte kan nås levereras till den när den väl blir möjlig att nå.\n\n</string>
|
||||
<string name="protocol_version">Protokollversion:</string>
|
||||
<string name="protocol_version_newer">Apparaten använder en nyare protokollversion</string>
|
||||
<string name="plugin_settings_with_name">Inställningar av %s</string>
|
||||
<string name="invalid_device_name">Ogiltigt apparatnamn</string>
|
||||
<string name="shareplugin_text_saved">Tog emot text, spara på klippbordet</string>
|
||||
<string name="custom_devices_settings">Egen apparatlista</string>
|
||||
<string name="custom_devices_settings_summary">%d apparater manuellt tillagda</string>
|
||||
<string name="custom_device_list">Lägg till apparater enligt IP-adress</string>
|
||||
<string name="custom_device_deleted">Egen apparat borttagen</string>
|
||||
<string name="custom_device_list_help">Om apparaten inte detekteras automatiskt kan dess IP-adress eller värddatornamn läggas till genom att klicka på den lösa åtgärdsknappen.</string>
|
||||
@@ -420,4 +423,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Fortsätt spela</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Visa en tyst underrättelse för att fortsätta spela på apparaten efter att media har stängts.</string>
|
||||
<string name="notification_channel_keepwatching">Fortsätt spela</string>
|
||||
<string name="ping_result">Skickade paket på %1$d millisekunder</string>
|
||||
<string name="ping_failed">Kunde inte skicka paket till apparaten</string>
|
||||
<string name="ping_in_progress">Skickar paket…</string>
|
||||
<string name="device_host_invalid">Värddatorn är ogiltig. Använd ett giltigt värddatornamn, IPv4 eller IPv6</string>
|
||||
<string name="device_host_duplicate">Värddatorn finns redan i listan</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Aygıt ulaşılabilir değil</string>
|
||||
<string name="error_already_paired">Aygıt zaten eşleşmiş</string>
|
||||
<string name="error_timed_out">Zaman aşımı</string>
|
||||
<string name="error_clocks_not_match">Aygıt saatleri eşzamanlı değil</string>
|
||||
<string name="error_canceled_by_user">Kullanıcı tarafından iptal edildi</string>
|
||||
<string name="error_canceled_by_other_peer">Diğer eş tarafından iptal edildi</string>
|
||||
<string name="encryption_info_title">Şifreleme Bilgisi</string>
|
||||
@@ -129,7 +130,7 @@
|
||||
</plurals>
|
||||
<plurals name="incoming_files_text">
|
||||
<item quantity="one">Dosya: %1s</item>
|
||||
<item quantity="other">(Dosya %2$d %3$d) : %1$s</item>
|
||||
<item quantity="other">(Dosya %2$d/%3$d) : %1$s</item>
|
||||
</plurals>
|
||||
<plurals name="outgoing_file_title">
|
||||
<item quantity="one">%1$d dosya %2$s gönderiliyor</item>
|
||||
@@ -164,9 +165,11 @@
|
||||
<string name="middle_click">Orta Tık Gönder</string>
|
||||
<string name="show_keyboard">Klavyeyi Göster</string>
|
||||
<string name="device_not_paired">Aygıt eşleşmemiş</string>
|
||||
<string name="pairing_duplicate_names">Dikkat: Aynı adlı birden çok aygıt var.</string>
|
||||
<string name="request_pairing">Eşleşme isteği</string>
|
||||
<string name="pairing_accept">Onayla</string>
|
||||
<string name="pairing_reject">Reddet</string>
|
||||
<string name="pairing_explanation">İki aygıtı eşleştirmek, onlara birbirlerine erişim hakkını verecektir. Yalnızca kendi aygıtlarınızı eşleştirin.</string>
|
||||
<string name="settings">Ayarlar</string>
|
||||
<string name="mpris_play">Oynat</string>
|
||||
<string name="mpris_pause">Duraklat</string>
|
||||
@@ -191,12 +194,15 @@
|
||||
<string name="mpris_notification_settings_summary">KDE Bağlan’ı açmadan ortam oynatıcılarınızı denetlemenize izin verin</string>
|
||||
<string name="share_to">Şuraya Paylaş…</string>
|
||||
<string name="unreachable_device">%s (Erişilebilir değil)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="unreachable_device_url_share_text">Erişilemeyen bir aygıta gönderilen URL’ler, aygıt erişilebilir olduğunda teslim edilir.\n\n</string>
|
||||
<string name="protocol_version">Protokol sürümü:</string>
|
||||
<string name="protocol_version_newer">Bu aygıt, daha yeni bir protokol sürümü kullanıyor</string>
|
||||
<string name="plugin_settings_with_name">%s ayarları</string>
|
||||
<string name="invalid_device_name">Geçersiz aygıt adı</string>
|
||||
<string name="shareplugin_text_saved">Gelen ileti, panoya kaydet</string>
|
||||
<string name="custom_devices_settings">Özel aygıt listesi</string>
|
||||
<string name="custom_devices_settings_summary">%d aygıt elle eklendi</string>
|
||||
<string name="custom_device_list">IP’ye göre aygıtları ekle</string>
|
||||
<string name="custom_device_deleted">Özel aygıt silindi</string>
|
||||
<string name="custom_device_list_help">Aygıtınız kendiliğinden algılanmazsa İşlem Düğmesine tıklayarak IP adresini veya ana bilgisayar adını ekleyebilirsiniz</string>
|
||||
@@ -306,6 +312,8 @@
|
||||
<string name="devices">Aygıtlar</string>
|
||||
<string name="settings_rename">Aygıt adı</string>
|
||||
<string name="settings_dark_mode">Karanlık tema</string>
|
||||
<string name="settings_export_logs">KDE Bağlan Günlüklerini Dışa Aktar</string>
|
||||
<string name="settings_export_logs_text">Sorun tanılama konusunda yandımcı olabilecek yürütme bilgisini içeren bir dosya üretin.</string>
|
||||
<string name="settings_more_settings_title">Daha fazla ayar</string>
|
||||
<string name="settings_more_settings_text">Aygıt başına ayarlar, bir aygıt içinden “Eklenti Ayarları” altında bulunabilir.</string>
|
||||
<string name="setting_persistent_notification">Kalıcı bildirim göster</string>
|
||||
@@ -396,7 +404,7 @@
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Hataları veya İsteklerinizi Bildirin</h1> <p>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.</p> <p>KDE’nin bir hata takip sistemi vardır. <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> adresini ziyaret edin veya hakkında ekranının ‘Hata Bildir’ düğmesini kullanarak hataları bildirin.</p> Bir iyileştirme için öneriniz varsa bunu bildirmek için hata takip sistemini kullanabilirsiniz; yalnızca “Wishlist” önceliğini kullandığınızdan emin olun.</string>
|
||||
<string name="about_kde_join_kde">"<h1>KDE’ye Katılın</h1> <p>KDE takımının bir üyesi olmak için yazılım geliştirici olmanıza gerek yoktur. Program arayüzlerini çeviren dil takımlarına katılabilirsiniz. Grafikler, temalar, sesler ve iyileştirilmiş belgelendirme sağlayabilirsiniz. Karar sizin!</p> <p>Katılabileceğiniz bazı projeler hakkında bilgi almak için <a href=https://community.kde.org/Get_Involved>https://community.kde.org/Get_Involved</a> sayfasını ziyaret edin.</p> Daha fazla bilgiye veya belgeye gereksiniminiz varsa <a href=https://techbase.kde.org/>https://techbase.kde.org/</a> sayfasında aradığınızı bulabilirsiniz."</string>
|
||||
<string name="about_kde_support_kde">"<h1>KDE’yi Destekleyin</h1> <p>KDE yazılımları her zaman ücretsiz kalmayı sürdürecektir; ancak bunu oluşturmak bedava değildir. </p> <p>Geliştirmeyi desteklemek için KDE topluluğu, kar amacı gütmeyen bir kuruluş olan KDE e.V.’yi kurmuştur, bu topluluk KDE topluğunu yasal ve finansal konularda temsil eder. KDE e.V. hakkında daha fazla bilgi için <a href=https://ev.kde.org/>https://ev.kde.org/</a> adresini ziyaret edin.</p> <p>KDE, finansal da dahil olmak üzere her türlü katkıdan yarar sağlar. Maddi kaynaklarımızla, geliştiricilerimizin ve diğerlerinin katkıda bulunurken oluşan masraflarını karşılıyoruz. Ayrıca yasal destek ve konferanslar ve toplantılar için de kullanılmaktadır.</p> <p>Emeklerimizi, finansal destekle desteklemeniz için <a href=https://www.kde.org/community/donations/>https://www.kde.org/community/donations/</a> adresinde bulunan yollardan birini kullanabilirsiniz.</p> Desteğiniz için şimdiden teşekkürler."</string>
|
||||
<string name="maintainer_and_developer">Projeyi sürdüren ve geliştirici</string>
|
||||
<string name="maintainer_and_developer">Bakımcı ve geliştirici</string>
|
||||
<string name="developer">Geliştirici</string>
|
||||
<string name="apple_support">macOS ve iOS desteği üzerinde çalışılmaktadır.</string>
|
||||
<string name="bug_fixes_and_general_improvements">Hata düzeltmeleri ve genel iyileştirmeler</string>
|
||||
@@ -420,4 +428,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Oynamayı Sürdür</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Ortamı kapattıktan sonra bu aygıtta oynatmayı sürdürmek için sessiz bir bildirim göster</string>
|
||||
<string name="notification_channel_keepwatching">Oynamayı Sürdür</string>
|
||||
<string name="ping_result">%1$d milisaniye içinde pinglendi</string>
|
||||
<string name="ping_failed">Aygıt pinglenemedi</string>
|
||||
<string name="ping_in_progress">Pingleniyor…</string>
|
||||
<string name="device_host_invalid">Makine geçersiz. Geçerli bir makine adı kullanın; IPv4 veya IPv6 gibi</string>
|
||||
<string name="device_host_duplicate">Makine listede halihazırda var</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">Немає доступу до пристрою</string>
|
||||
<string name="error_already_paired">Пристрій вже пов’язано</string>
|
||||
<string name="error_timed_out">Час очікування вичерпано</string>
|
||||
<string name="error_clocks_not_match">Годинник пристрою не синхронізовано</string>
|
||||
<string name="error_canceled_by_user">Скасовано користувачем</string>
|
||||
<string name="error_canceled_by_other_peer">Скасовано з іншого вузла пов’язування</string>
|
||||
<string name="encryption_info_title">Дані щодо шифрування</string>
|
||||
@@ -180,9 +181,11 @@
|
||||
<string name="middle_click">Надіслати клацання лівою кнопкою</string>
|
||||
<string name="show_keyboard">Показати клавіатуру</string>
|
||||
<string name="device_not_paired">Пристрій не пов’язано</string>
|
||||
<string name="pairing_duplicate_names">Попередження: маємо декілька пристроїв із однією назвою.</string>
|
||||
<string name="request_pairing">Надіслати запит щодо пов’язування</string>
|
||||
<string name="pairing_accept">Прийняти</string>
|
||||
<string name="pairing_reject">Відмовити</string>
|
||||
<string name="pairing_explanation">Пов\'язування двох пристроїв надасть їм взаємний доступ. Пов\'язуйте між собою лише ваші власні пристрої.</string>
|
||||
<string name="settings">Параметри</string>
|
||||
<string name="mpris_play">Пуск</string>
|
||||
<string name="mpris_pause">Пауза</string>
|
||||
@@ -207,12 +210,15 @@
|
||||
<string name="mpris_notification_settings_summary">Уможливлює керування відтворенням мультимедійних даних без відкриття KDE Connect</string>
|
||||
<string name="share_to">Оприлюднити на…</string>
|
||||
<string name="unreachable_device">%s (недоступний)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="unreachable_device_url_share_text">Адреси, які оприлюднено на недоступному пристрої, буде надіслано, щойно пристрій стане доступним.\n\n</string>
|
||||
<string name="protocol_version">Версія протоколу:</string>
|
||||
<string name="protocol_version_newer">На цьому пристрої використовується новіша версія протоколу</string>
|
||||
<string name="plugin_settings_with_name">Параметри %s</string>
|
||||
<string name="invalid_device_name">Некоректна назва пристрою</string>
|
||||
<string name="shareplugin_text_saved">Отримано текст, збережено до буфера обміну даними</string>
|
||||
<string name="custom_devices_settings">Список нетипових пристроїв</string>
|
||||
<string name="custom_devices_settings_summary">%d пристроїв додано вручну</string>
|
||||
<string name="custom_device_list">Додати пристрої за IP</string>
|
||||
<string name="custom_device_deleted">Нетиповий пристрій вилучено</string>
|
||||
<string name="custom_device_list_help">Якщо ваш пристрій не було виявлено автоматично, ви можете додати його IP-адресу або назву вузла, натиснувши рухому кнопку дій.</string>
|
||||
@@ -322,6 +328,8 @@
|
||||
<string name="devices">Пристрої</string>
|
||||
<string name="settings_rename">Назва пристрою</string>
|
||||
<string name="settings_dark_mode">Темна тема</string>
|
||||
<string name="settings_export_logs">Експортувати журнал KDE Connect</string>
|
||||
<string name="settings_export_logs_text">Створити файл з даними щодо виконання, які можуть допомогти у діагностиці вад.</string>
|
||||
<string name="settings_more_settings_title">Додаткові параметри</string>
|
||||
<string name="settings_more_settings_text">Окремі параметри пристроїв наведено на сторінці «Параметри додатків» сторінки пристрою.</string>
|
||||
<string name="setting_persistent_notification">Показувати постійне сповіщення</string>
|
||||
@@ -436,4 +444,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">Продовжити відтворення</string>
|
||||
<string name="mpris_keepwatching_settings_summary">Показати беззвучне сповіщення для продовження відтворення на цьому пристрої після закриття носія</string>
|
||||
<string name="notification_channel_keepwatching">Продовжити відтворення</string>
|
||||
<string name="ping_result">Виконано зондування за %1$d мілісекунд</string>
|
||||
<string name="ping_failed">Не вдалося зондувати пристрій</string>
|
||||
<string name="ping_in_progress">Зондування…</string>
|
||||
<string name="device_host_invalid">Вузол є некоректним. Скористайтеся коректною назвою вузла, IPv4 або IPv6</string>
|
||||
<string name="device_host_duplicate">Запис вузла вже є у списку</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">设备不可及</string>
|
||||
<string name="error_already_paired">设备已配对</string>
|
||||
<string name="error_timed_out">超时</string>
|
||||
<string name="error_clocks_not_match">设备时钟不同步</string>
|
||||
<string name="error_canceled_by_user">已被用户取消</string>
|
||||
<string name="error_canceled_by_other_peer">已被其他对等点取消</string>
|
||||
<string name="encryption_info_title">加密信息</string>
|
||||
@@ -156,9 +157,11 @@
|
||||
<string name="middle_click">发送中键点击</string>
|
||||
<string name="show_keyboard">显示键盘</string>
|
||||
<string name="device_not_paired">设备未配对</string>
|
||||
<string name="pairing_duplicate_names">注意:存在多个同名设备。</string>
|
||||
<string name="request_pairing">请求配对</string>
|
||||
<string name="pairing_accept">接受</string>
|
||||
<string name="pairing_reject">拒绝</string>
|
||||
<string name="pairing_explanation">两个设备在配对后即可相互访问。请只配对您自己的设备。</string>
|
||||
<string name="settings">设置</string>
|
||||
<string name="mpris_play">播放</string>
|
||||
<string name="mpris_pause">暂停</string>
|
||||
@@ -183,12 +186,15 @@
|
||||
<string name="mpris_notification_settings_summary">不打开 KDE Connect 也能在常驻通知中控制媒体播放器</string>
|
||||
<string name="share_to">分享到…</string>
|
||||
<string name="unreachable_device">%s (无法访问)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="unreachable_device_url_share_text">URL 被分享到了不可访问的设备。它将在设备能够访问时自动传输到该设备。\n</string>
|
||||
<string name="protocol_version">协议版本:</string>
|
||||
<string name="protocol_version_newer">此设备使用较新版本的协议</string>
|
||||
<string name="plugin_settings_with_name">%s设置</string>
|
||||
<string name="invalid_device_name">无效的设备名</string>
|
||||
<string name="shareplugin_text_saved">已收到文本,存至剪贴板</string>
|
||||
<string name="custom_devices_settings">自定义设备列表</string>
|
||||
<string name="custom_devices_settings_summary">手动添加了 %d 个设备</string>
|
||||
<string name="custom_device_list">通过 IP 添加设备</string>
|
||||
<string name="custom_device_deleted">自定义设备已删除</string>
|
||||
<string name="custom_device_list_help">如果您的设备未被自动检测到,您点击浮动操作按钮可以添加它的 IP 地址或主机名。</string>
|
||||
@@ -298,6 +304,8 @@
|
||||
<string name="devices">设备</string>
|
||||
<string name="settings_rename">设备名</string>
|
||||
<string name="settings_dark_mode">深色主题</string>
|
||||
<string name="settings_export_logs">导出 KDE Connect 日志</string>
|
||||
<string name="settings_export_logs_text">生成一个包含程序执行信息的文件,有助于排查故障。</string>
|
||||
<string name="settings_more_settings_title">更多设置</string>
|
||||
<string name="settings_more_settings_text">每个设备的独立设置可以在设备页内的“插件设置”下找到。</string>
|
||||
<string name="setting_persistent_notification">启用常驻通知栏</string>
|
||||
@@ -412,4 +420,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">继续播放</string>
|
||||
<string name="mpris_keepwatching_settings_summary">关闭媒体后显示一条用于继续在此设备上播放的静音通知</string>
|
||||
<string name="notification_channel_keepwatching">继续播放</string>
|
||||
<string name="ping_result">Ping 的响应时间是 %1$d 毫秒</string>
|
||||
<string name="ping_failed">无法 ping 设备</string>
|
||||
<string name="ping_in_progress">正在执行 ping 命令…</string>
|
||||
<string name="device_host_invalid">主机无效。请使用有效的主机名、IPv4 或 IPv6 地址</string>
|
||||
<string name="device_host_duplicate">主机已在列表中</string>
|
||||
</resources>
|
||||
|
@@ -115,6 +115,7 @@
|
||||
<string name="error_not_reachable">裝置無法存取</string>
|
||||
<string name="error_already_paired">裝置已經配對</string>
|
||||
<string name="error_timed_out">逾時</string>
|
||||
<string name="error_clocks_not_match">裝置時鐘不同步</string>
|
||||
<string name="error_canceled_by_user">使用者中斷</string>
|
||||
<string name="error_canceled_by_other_peer">被其他同等功能應用中斷</string>
|
||||
<string name="encryption_info_title">加密資訊</string>
|
||||
@@ -184,11 +185,13 @@
|
||||
<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">協定版本:</string>
|
||||
<string name="protocol_version_newer">此裝置使用較新的通訊協定版本</string>
|
||||
<string name="plugin_settings_with_name">%s的設定</string>
|
||||
<string name="invalid_device_name">無效的裝置名稱</string>
|
||||
<string name="shareplugin_text_saved">已接收文字,並且儲存到剪貼簿</string>
|
||||
<string name="custom_devices_settings">自訂裝置列表</string>
|
||||
<string name="custom_devices_settings_summary">已成功加入 %d 台裝置</string>
|
||||
<string name="custom_device_list">透過 IP 新增裝置</string>
|
||||
<string name="custom_device_deleted">自訂裝置已刪除</string>
|
||||
<string name="custom_device_list_help">如果未自動偵測到您的裝置,您可以透過點擊「浮動操作按鈕」來新增其 IP 位址或主機名稱</string>
|
||||
@@ -298,6 +301,8 @@
|
||||
<string name="devices">裝置</string>
|
||||
<string name="settings_rename">裝置名稱</string>
|
||||
<string name="settings_dark_mode">暗色主題</string>
|
||||
<string name="settings_export_logs">匯出 KDE Connect 紀錄</string>
|
||||
<string name="settings_export_logs_text">產生包含執行資訊的檔案來幫助發現問題。</string>
|
||||
<string name="settings_more_settings_title">更多設定</string>
|
||||
<string name="settings_more_settings_text">各裝置設定可在裝置內的「外掛程式設定」底下找到。</string>
|
||||
<string name="setting_persistent_notification">顯示一致設定</string>
|
||||
@@ -412,4 +417,9 @@
|
||||
<string name="mpris_keepwatching_settings_title">繼續播放</string>
|
||||
<string name="mpris_keepwatching_settings_summary">顯示無聲通知以在關閉媒體後繼續在此裝置上播放</string>
|
||||
<string name="notification_channel_keepwatching">繼續播放</string>
|
||||
<string name="ping_result">成功傳送測試封包,花費 %1$d 毫秒</string>
|
||||
<string name="ping_failed">無法向裝置傳送測試封包</string>
|
||||
<string name="ping_in_progress">正在傳送測試封包…</string>
|
||||
<string name="device_host_invalid">主機是無效的。請使用有效的主機名稱、IPv4 位址或 IPv6 位址</string>
|
||||
<string name="device_host_duplicate">主機已在清單中</string>
|
||||
</resources>
|
||||
|
@@ -176,6 +176,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<string name="error_not_reachable">Device not reachable</string>
|
||||
<string name="error_already_paired">Device already paired</string>
|
||||
<string name="error_timed_out">Timed out</string>
|
||||
<string name="error_clocks_not_match">Device clocks are out of sync</string>
|
||||
<string name="error_canceled_by_user">Canceled by user</string>
|
||||
<string name="error_canceled_by_other_peer">Canceled by other peer</string>
|
||||
<string name="encryption_info_title">Encryption Info</string>
|
||||
@@ -226,9 +227,11 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<string name="middle_click">Send Middle Click</string>
|
||||
<string name="show_keyboard">Show Keyboard</string>
|
||||
<string name="device_not_paired">Device not paired</string>
|
||||
<string name="pairing_duplicate_names">Caution: There are multiple devices with the same name.</string>
|
||||
<string name="request_pairing">Request pairing</string>
|
||||
<string name="pairing_accept">Accept</string>
|
||||
<string name="pairing_reject">Reject</string>
|
||||
<string name="pairing_explanation">Pairing two devices will give them access to each other. Only pair together your own devices.</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="mpris_play">Play</string>
|
||||
<string name="mpris_pause">Pause</string>
|
||||
@@ -263,12 +266,15 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<string name="mpris_notification_key" translatable="false">mpris_notification_enabled</string>
|
||||
<string name="share_to">Share to…</string>
|
||||
<string name="unreachable_device">%s (Unreachable)</string>
|
||||
<string name="unreachable_device_dynamic_shortcut">%s (✕)</string>
|
||||
<string name="unreachable_device_url_share_text">URLs shared to an unreachable device will be delivered to it once it becomes reachable.\n\n</string>
|
||||
<string name="protocol_version">Protocol version:</string>
|
||||
<string name="protocol_version_newer">This device uses a newer protocol version</string>
|
||||
<string name="plugin_settings_with_name">%s settings</string>
|
||||
<string name="invalid_device_name">Invalid device name</string>
|
||||
<string name="shareplugin_text_saved">Received text, saved to clipboard</string>
|
||||
<string name="custom_devices_settings">Custom device list</string>
|
||||
<string name="custom_devices_settings_summary">%d devices added manually</string>
|
||||
<string name="custom_device_list">Add devices by IP</string>
|
||||
<string name="custom_device_deleted">Custom device deleted</string>
|
||||
<string name="custom_device_list_help">If your device is not automatically detected you can add its IP address or hostname by clicking on the Floating Action Button</string>
|
||||
@@ -398,6 +404,9 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
|
||||
<string name="settings_rename">Device name</string>
|
||||
<string name="settings_dark_mode">Dark theme</string>
|
||||
<string name="settings_export_logs">Export KDE Connect logs</string>
|
||||
<string name="settings_export_logs_text">Generate a file with execution information that can help troubleshoot issues.</string>
|
||||
|
||||
<string name="settings_more_settings_title">More settings</string>
|
||||
<string name="settings_more_settings_text">Per-device settings can be found under \'Plugin settings\' from within a device.</string>
|
||||
<string name="setting_persistent_notification">Show persistent notification</string>
|
||||
@@ -581,4 +590,11 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
|
||||
<string name="mpris_keepwatching_settings_summary">Show a silent notification to continue playing on this device after closing media</string>
|
||||
<string name="notification_channel_keepwatching">Continue playing</string>
|
||||
|
||||
<string name="ping_result">Pinged in %1$d milliseconds</string>
|
||||
<string name="ping_failed">Could not ping device</string>
|
||||
<string name="ping_in_progress">Pinging…</string>
|
||||
|
||||
<string name="device_host_invalid">Host is invalid. Use a valid hostname, IPv4, or IPv6</string>
|
||||
<string name="device_host_duplicate">Host already exists in the list</string>
|
||||
|
||||
</resources>
|
||||
|
@@ -3,11 +3,9 @@ pluginManagement {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
/* Needed for org.apache.sshd debugging
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
|
@@ -24,13 +24,16 @@ import java.io.Reader
|
||||
import java.util.UUID
|
||||
import kotlin.text.Charsets.UTF_8
|
||||
|
||||
class BluetoothLink(context: Context?, connection: ConnectionMultiplexer, input: InputStream, output: OutputStream, remoteAddress: BluetoothDevice, deviceInfo: DeviceInfo, linkProvider: BluetoothLinkProvider) : BaseLink(context!!, linkProvider) {
|
||||
private val connection: ConnectionMultiplexer?
|
||||
private val input: InputStream
|
||||
private val output: OutputStream
|
||||
private val remoteAddress: BluetoothDevice
|
||||
private val linkProvider: BluetoothLinkProvider
|
||||
private val deviceInfo: DeviceInfo
|
||||
class BluetoothLink(
|
||||
context: Context?,
|
||||
connection: ConnectionMultiplexer,
|
||||
val input: InputStream,
|
||||
val output: OutputStream,
|
||||
val remoteAddress: BluetoothDevice,
|
||||
val theDeviceInfo: DeviceInfo,
|
||||
val linkProvider: BluetoothLinkProvider
|
||||
) : BaseLink(context!!, linkProvider) {
|
||||
private val connection: ConnectionMultiplexer? = connection
|
||||
private var continueAccepting = true
|
||||
private val receivingThread = Thread(object : Runnable {
|
||||
override fun run() {
|
||||
@@ -64,8 +67,7 @@ class BluetoothLink(context: Context?, connection: ConnectionMultiplexer, input:
|
||||
}
|
||||
|
||||
private fun processMessage(message: String) {
|
||||
val np: NetworkPacket
|
||||
np = try {
|
||||
val np = try {
|
||||
NetworkPacket.unserialize(message)
|
||||
} catch (e: JSONException) {
|
||||
Log.e("BluetoothLink/receiving", "Unable to parse message.", e)
|
||||
@@ -84,15 +86,6 @@ class BluetoothLink(context: Context?, connection: ConnectionMultiplexer, input:
|
||||
}
|
||||
})
|
||||
|
||||
init {
|
||||
this.connection = connection
|
||||
this.input = input
|
||||
this.output = output
|
||||
this.deviceInfo = deviceInfo
|
||||
this.remoteAddress = remoteAddress
|
||||
this.linkProvider = linkProvider
|
||||
}
|
||||
|
||||
fun startListening() {
|
||||
receivingThread.start()
|
||||
}
|
||||
@@ -102,7 +95,7 @@ class BluetoothLink(context: Context?, connection: ConnectionMultiplexer, input:
|
||||
}
|
||||
|
||||
override fun getDeviceInfo(): DeviceInfo {
|
||||
return deviceInfo
|
||||
return theDeviceInfo
|
||||
}
|
||||
|
||||
override fun disconnect() {
|
||||
|
@@ -35,6 +35,7 @@ import java.io.Reader
|
||||
import java.security.cert.CertificateException
|
||||
import java.util.UUID
|
||||
import kotlin.text.Charsets.UTF_8
|
||||
import androidx.core.content.edit
|
||||
|
||||
class BluetoothLinkProvider(private val context: Context) : BaseLinkProvider() {
|
||||
private val visibleDevices: MutableMap<String, BluetoothLink> = HashMap()
|
||||
@@ -138,9 +139,9 @@ class BluetoothLinkProvider(private val context: Context) : BaseLinkProvider() {
|
||||
} catch (e: SecurityException) {
|
||||
Log.e("KDEConnect", "Security Exception for CONNECT", e)
|
||||
|
||||
val prefenceEditor = PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||
prefenceEditor.putBoolean(SettingsFragment.KEY_BLUETOOTH_ENABLED, false)
|
||||
prefenceEditor.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putBoolean(SettingsFragment.KEY_BLUETOOTH_ENABLED, false)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
private class ChannelInputStream constructor(val channel: Channel) : InputStream(), Closeable {
|
||||
private class ChannelInputStream(val channel: Channel) : InputStream(), Closeable {
|
||||
override fun available(): Int {
|
||||
return channel.available()
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
private class ChannelOutputStream constructor(val channel: Channel) : OutputStream(), Closeable {
|
||||
private class ChannelOutputStream(val channel: Channel) : OutputStream(), Closeable {
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
channel.close()
|
||||
@@ -78,7 +78,7 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
private class Channel constructor(val multiplexer: ConnectionMultiplexer, val id: UUID) : Closeable {
|
||||
private class Channel(val multiplexer: ConnectionMultiplexer, val id: UUID) : Closeable {
|
||||
val readBuffer: ByteBuffer = ByteBuffer.allocate(BUFFER_SIZE)
|
||||
val lock = ReentrantLock()
|
||||
var lockCondition: Condition = lock.newCondition()
|
||||
@@ -371,14 +371,9 @@ class ConnectionMultiplexer(socket: BluetoothSocket) : Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ListenRunnable constructor(socket: BluetoothSocket) : Runnable {
|
||||
var input: InputStream
|
||||
var output: OutputStream
|
||||
|
||||
init {
|
||||
input = socket.inputStream
|
||||
output = socket.outputStream
|
||||
}
|
||||
private inner class ListenRunnable(socket: BluetoothSocket) : Runnable {
|
||||
var input: InputStream = socket.inputStream
|
||||
var output: OutputStream = socket.outputStream
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun readBuffer(buffer: ByteArray, len: Int) {
|
||||
|
@@ -10,8 +10,8 @@ import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Network;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
@@ -19,20 +19,17 @@ import androidx.annotation.WorkerThread;
|
||||
import org.json.JSONException;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.DeviceHost;
|
||||
import org.kde.kdeconnect.DeviceInfo;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper;
|
||||
import org.kde.kdeconnect.Helpers.TrustedNetworkHelper;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.UserInterface.CustomDevicesActivity;
|
||||
import org.kde.kdeconnect.UserInterface.SettingsFragment;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
@@ -58,7 +55,7 @@ import kotlin.text.Charsets;
|
||||
* WiFi network. The first packet sent over a socket must be an
|
||||
* {@link DeviceInfo#toIdentityPacket()}.
|
||||
*
|
||||
* @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted)
|
||||
* @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted, boolean)
|
||||
*/
|
||||
public class LanLinkProvider extends BaseLinkProvider {
|
||||
|
||||
@@ -67,20 +64,23 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
final static int MAX_PORT = 1764;
|
||||
final static int PAYLOAD_TRANSFER_MIN_PORT = 1739;
|
||||
|
||||
final static int MAX_IDENTITY_PACKET_SIZE = 1024 * 512;
|
||||
final static int MAX_UDP_PACKET_SIZE = 1024 * 512;
|
||||
|
||||
final static long MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE = 500L;
|
||||
final static long MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE = 1000L;
|
||||
|
||||
private final Context context;
|
||||
|
||||
final HashMap<String, LanLink> visibleDevices = new HashMap<>(); // Links by device id
|
||||
|
||||
final ConcurrentHashMap<String, Long> lastConnectionTime = new ConcurrentHashMap<>();
|
||||
final static int MAX_RATE_LIMIT_ENTRIES = 255;
|
||||
final ConcurrentHashMap<String, Long> lastConnectionTimeByDeviceId = new ConcurrentHashMap<>();
|
||||
final ConcurrentHashMap<InetAddress, Long> lastConnectionTimeByIp = new ConcurrentHashMap<>();
|
||||
|
||||
private ServerSocket tcpServer;
|
||||
private DatagramSocket udpServer;
|
||||
|
||||
private MdnsDiscovery mdnsDiscovery;
|
||||
private final MdnsDiscovery mdnsDiscovery;
|
||||
|
||||
private long lastBroadcast = 0;
|
||||
private final static long delayBetweenBroadcasts = 200;
|
||||
@@ -93,30 +93,115 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
super.onConnectionLost(link);
|
||||
}
|
||||
|
||||
Pair<NetworkPacket, Boolean> unserializeReceivedIdentityPacket(String message) {
|
||||
NetworkPacket identityPacket;
|
||||
try {
|
||||
identityPacket = NetworkPacket.unserialize(message);
|
||||
} catch (JSONException e) {
|
||||
Log.w("KDE/LanLinkProvider", "Invalid identity packet received: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!DeviceInfo.isValidIdentityPacket(identityPacket)) {
|
||||
Log.w("KDE/LanLinkProvider", "Invalid identity packet received.");
|
||||
return null;
|
||||
}
|
||||
|
||||
final String deviceId = identityPacket.getString("deviceId");
|
||||
String myId = DeviceHelper.getDeviceId(context);
|
||||
if (deviceId.equals(myId)) {
|
||||
//Ignore my own broadcast
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rateLimitByDeviceId(deviceId)) {
|
||||
Log.i("LanLinkProvider", "Discarding second packet from the same device " + deviceId + " received too quickly");
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean deviceTrusted = isDeviceTrusted(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 null;
|
||||
}
|
||||
|
||||
return new Pair<>(identityPacket, deviceTrusted);
|
||||
}
|
||||
|
||||
//They received my UDP broadcast and are connecting to me. The first thing they send should be their identity packet.
|
||||
@WorkerThread
|
||||
private void tcpPacketReceived(Socket socket) throws IOException {
|
||||
|
||||
NetworkPacket networkPacket;
|
||||
InetAddress address = socket.getInetAddress();
|
||||
if (rateLimitByIp(address)) {
|
||||
Log.i("LanLinkProvider", "Discarding second TCP packet from the same ip " + address + " received too quickly");
|
||||
return;
|
||||
}
|
||||
|
||||
String message;
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String message = reader.readLine();
|
||||
networkPacket = NetworkPacket.unserialize(message);
|
||||
//Log.e("TcpListener", "Received TCP packet: " + networkPacket.serialize());
|
||||
message = readSingleLine(socket);
|
||||
//Log.e("TcpListener", "Received TCP packet: " + identityPacket.serialize());
|
||||
} catch (Exception e) {
|
||||
Log.e("KDE/LanLinkProvider", "Exception while receiving TCP packet", e);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i("KDE/LanLinkProvider", "identity packet received from a TCP connection from " + networkPacket.getString("deviceName"));
|
||||
|
||||
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.");
|
||||
final Pair<NetworkPacket, Boolean> pair = unserializeReceivedIdentityPacket(message);
|
||||
if (pair == null) {
|
||||
return;
|
||||
}
|
||||
final NetworkPacket identityPacket = pair.first;
|
||||
final boolean deviceTrusted = pair.second;
|
||||
|
||||
identityPacketReceived(networkPacket, socket, LanLink.ConnectionStarted.Locally, deviceTrusted);
|
||||
Log.i("KDE/LanLinkProvider", "identity packet received from a TCP connection from " + identityPacket.getString("deviceName"));
|
||||
|
||||
identityPacketReceived(identityPacket, socket, LanLink.ConnectionStarted.Locally, deviceTrusted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single line from a socket without consuming anything else from the input.
|
||||
*/
|
||||
private String readSingleLine(Socket socket) throws IOException {
|
||||
InputStream stream = socket.getInputStream();
|
||||
StringBuilder line = new StringBuilder(MAX_IDENTITY_PACKET_SIZE);
|
||||
int ch;
|
||||
while ((ch = stream.read()) != -1) {
|
||||
line.append((char) ch);
|
||||
if (ch == '\n') {
|
||||
return line.toString();
|
||||
}
|
||||
if (line.length() >= MAX_IDENTITY_PACKET_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new IOException("Couldn't read a line from the socket");
|
||||
}
|
||||
|
||||
boolean rateLimitByIp(InetAddress address) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastConnectionTimeByIp.get(address);
|
||||
if (last != null && (last + MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE > now)) {
|
||||
return true;
|
||||
}
|
||||
lastConnectionTimeByIp.put(address, now);
|
||||
if (lastConnectionTimeByIp.size() > MAX_RATE_LIMIT_ENTRIES) {
|
||||
lastConnectionTimeByIp.entrySet().removeIf(e -> e.getValue() + MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE < now);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean rateLimitByDeviceId(String deviceId) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastConnectionTimeByDeviceId.get(deviceId);
|
||||
if (last != null && (last + MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE > now)) {
|
||||
return true;
|
||||
}
|
||||
lastConnectionTimeByDeviceId.put(deviceId, now);
|
||||
if (lastConnectionTimeByDeviceId.size() > MAX_RATE_LIMIT_ENTRIES) {
|
||||
lastConnectionTimeByDeviceId.entrySet().removeIf(e -> e.getValue() + MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE < now);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//I've received their broadcast and should connect to their TCP socket and send my identity.
|
||||
@@ -125,28 +210,21 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
|
||||
final InetAddress address = packet.getAddress();
|
||||
|
||||
if (rateLimitByIp(address)) {
|
||||
Log.i("LanLinkProvider", "Discarding second UDP packet from the same ip " + address + " received too quickly");
|
||||
return;
|
||||
}
|
||||
|
||||
String message = new String(packet.getData(), Charsets.UTF_8);
|
||||
final NetworkPacket identityPacket = NetworkPacket.unserialize(message);
|
||||
|
||||
if (!DeviceInfo.isValidIdentityPacket(identityPacket)) {
|
||||
Log.w("KDE/LanLinkProvider", "Invalid identity packet received.");
|
||||
final Pair<NetworkPacket, Boolean> pair = unserializeReceivedIdentityPacket(message);
|
||||
if (pair == null) {
|
||||
return;
|
||||
}
|
||||
final NetworkPacket identityPacket = pair.first;
|
||||
final boolean deviceTrusted = pair.second;
|
||||
|
||||
final String deviceId = identityPacket.getString("deviceId");
|
||||
String myId = DeviceHelper.getDeviceId(context);
|
||||
if (deviceId.equals(myId)) {
|
||||
//Ignore my own broadcast
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastConnectionTime.get(deviceId);
|
||||
if (last != null && (last + MILLIS_DELAY_BETWEEN_CONNECTIONS_TO_SAME_DEVICE > now)) {
|
||||
Log.i("LanLinkProvider", "Discarding second UDP packet from the same device " + deviceId + " received too quickly");
|
||||
return;
|
||||
}
|
||||
lastConnectionTime.put(deviceId, now);
|
||||
Log.i("KDE/LanLinkProvider", "Broadcast identity packet received from " + identityPacket.getString("deviceName"));
|
||||
|
||||
int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT);
|
||||
if (tcpPort < MIN_PORT || tcpPort > MAX_PORT) {
|
||||
@@ -154,14 +232,6 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -192,12 +262,7 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
/**
|
||||
* Called when a new 'identity' packet is received. Those are passed here by
|
||||
* {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}.
|
||||
* <p>
|
||||
* Should be called on a new thread since it blocks until the handshake is completed.
|
||||
* </p><p>
|
||||
* If the remote device should be connected, this calls {@link #addLink}.
|
||||
* Otherwise, if there was an Exception, we unpair from that device.
|
||||
* </p>
|
||||
*
|
||||
* @param identityPacket identity of a remote device
|
||||
* @param socket a new Socket, which should be used to receive packets from the remote device
|
||||
@@ -206,52 +271,60 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
*/
|
||||
@WorkerThread
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
|
||||
String myId = DeviceHelper.getDeviceId(context);
|
||||
final String deviceId = identityPacket.getString("deviceId");
|
||||
if (deviceId.equals(myId)) {
|
||||
Log.e("KDE/LanLinkProvider", "Somehow I'm connected to myself, ignoring. This should not happen.");
|
||||
|
||||
int protocolVersion = identityPacket.getInt("protocolVersion");
|
||||
if (deviceTrusted && isProtocolDowngrade(deviceId, protocolVersion)) {
|
||||
Log.w("KDE/LanLinkProvider", "Refusing to connect to a device using an older protocol version:" + protocolVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
// If I'm the TCP server I will be the SSL client and viceversa.
|
||||
final boolean clientMode = (connectionStarted == LanLink.ConnectionStarted.Locally);
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
device.unpair();
|
||||
//Retry as unpaired
|
||||
identityPacketReceived(identityPacket, socket, connectionStarted, deviceTrusted);
|
||||
Log.e("KDE/LanLinkProvider", "Device trusted but no cert stored. This should not happen.");
|
||||
return;
|
||||
}
|
||||
|
||||
String deviceName = identityPacket.getString("deviceName", "unknown");
|
||||
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + deviceName + " trusted:" + deviceTrusted);
|
||||
|
||||
// If I'm the TCP server I will be the SSL client and viceversa.
|
||||
final boolean clientMode = (connectionStarted == LanLink.ConnectionStarted.Locally);
|
||||
final SSLSocket sslSocket = SslHelper.convertToSslSocket(context, socket, deviceId, deviceTrusted, clientMode);
|
||||
sslSocket.addHandshakeCompletedListener(event -> {
|
||||
String mode = clientMode ? "client" : "server";
|
||||
try {
|
||||
Certificate certificate = event.getPeerCertificates()[0];
|
||||
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
|
||||
Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + deviceName + " secured with " + event.getCipherSuite());
|
||||
addOrUpdateLink(sslSocket, deviceInfo);
|
||||
} catch (IOException e) {
|
||||
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + deviceName, e);
|
||||
Device device = KdeConnect.getInstance().getDevice(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
// Start a new thread because some Android versions don't allow calling sslSocket.getOutputStream() from the callback
|
||||
ThreadHelper.execute(() -> {
|
||||
String mode = clientMode ? "client" : "server";
|
||||
try {
|
||||
NetworkPacket secureIdentityPacket;
|
||||
if (protocolVersion >= 8) {
|
||||
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
|
||||
NetworkPacket myIdentity = myDeviceInfo.toIdentityPacket();
|
||||
OutputStream writer = sslSocket.getOutputStream();
|
||||
writer.write(myIdentity.serialize().getBytes(Charsets.UTF_8));
|
||||
writer.flush();
|
||||
String line = readSingleLine(sslSocket);
|
||||
// Do not trust the identity packet we received unencrypted
|
||||
secureIdentityPacket = NetworkPacket.unserialize(line);
|
||||
if (!DeviceInfo.isValidIdentityPacket(secureIdentityPacket)) {
|
||||
throw new JSONException("Invalid identity packet");
|
||||
}
|
||||
int newProtocolVersion = secureIdentityPacket.getInt("protocolVersion");
|
||||
if (newProtocolVersion != protocolVersion) {
|
||||
Log.w("KDE/LanLinkProvider", "Protocol version changed half-way through the handshake: " + protocolVersion + " ->" + newProtocolVersion);
|
||||
}
|
||||
} else {
|
||||
secureIdentityPacket = identityPacket;
|
||||
}
|
||||
Certificate certificate = event.getPeerCertificates()[0];
|
||||
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(secureIdentityPacket, certificate);
|
||||
Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + deviceName + " secured with " + event.getCipherSuite());
|
||||
addOrUpdateLink(sslSocket, deviceInfo);
|
||||
} catch (JSONException e) {
|
||||
Log.e("KDE/LanLinkProvider", "Remote device doesn't correctly implement protocol version 8", e);
|
||||
} catch (IOException e) {
|
||||
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + deviceName, e);
|
||||
}
|
||||
device.unpair();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection
|
||||
@@ -260,6 +333,12 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
Log.d("LanLinkProvider", "Handshake done");
|
||||
}
|
||||
|
||||
private boolean isProtocolDowngrade(String deviceId, int protocolVersion) {
|
||||
SharedPreferences devicePrefs = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
|
||||
int lastKnownProtocolVersion = devicePrefs.getInt("protocolVersion", 0);
|
||||
return lastKnownProtocolVersion > protocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a link in the {@link #visibleDevices} map.
|
||||
*
|
||||
@@ -377,26 +456,20 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
}
|
||||
|
||||
private void broadcastUdpIdentityPacket(@Nullable Network network) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (!preferences.getBoolean(SettingsFragment.KEY_UDP_BROADCAST_ENABLED, true)) {
|
||||
Log.i("LanLinkProvider", "UDP broadcast is disabled in settings. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.execute(() -> {
|
||||
List<String> ipStringList = CustomDevicesActivity
|
||||
.getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context));
|
||||
List<DeviceHost> hostList = CustomDevicesActivity
|
||||
.getCustomDeviceList(context);
|
||||
|
||||
if (TrustedNetworkHelper.isTrustedNetwork(context)) {
|
||||
ipStringList.add("255.255.255.255"); //Default: broadcast.
|
||||
hostList.add(DeviceHost.BROADCAST); //Default: broadcast.
|
||||
} else {
|
||||
Log.i("LanLinkProvider", "Current network isn't trusted, not broadcasting");
|
||||
}
|
||||
|
||||
ArrayList<InetAddress> ipList = new ArrayList<>();
|
||||
for (String ip : ipStringList) {
|
||||
for (DeviceHost host : hostList) {
|
||||
try {
|
||||
ipList.add(InetAddress.getByName(ip));
|
||||
ipList.add(InetAddress.getByName(host.toString()));
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -417,6 +490,8 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: In protocol version 8 this packet doesn't need to contain identity info
|
||||
// since it will be exchanged after the socket is encrypted.
|
||||
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
|
||||
NetworkPacket identity = myDeviceInfo.toIdentityPacket();
|
||||
identity.set("tcpPort", tcpServer.getLocalPort());
|
||||
@@ -470,7 +545,9 @@ public class LanLinkProvider extends BaseLinkProvider {
|
||||
setupTcpListener();
|
||||
|
||||
mdnsDiscovery.startDiscovering();
|
||||
mdnsDiscovery.startAnnouncing();
|
||||
if (TrustedNetworkHelper.isTrustedNetwork(context)) {
|
||||
mdnsDiscovery.startAnnouncing();
|
||||
}
|
||||
|
||||
broadcastUdpIdentityPacket(null);
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@ public class MdnsDiscovery {
|
||||
this.lanLinkProvider = lanLinkProvider;
|
||||
this.mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
|
||||
this.mNsdResolveQueue = new NsdResolveQueue(this.mNsdManager);
|
||||
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
|
||||
WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||
multicastLock = wifiManager.createMulticastLock("kdeConnectMdnsMulticastLock");
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ public class MdnsDiscovery {
|
||||
// Also, on Android Lollipop those fields aren't resolved.
|
||||
String deviceName = DeviceHelper.getDeviceName(context);
|
||||
String deviceType = DeviceHelper.getDeviceType().toString();
|
||||
String protocolVersion = Integer.toString(DeviceHelper.ProtocolVersion);
|
||||
String protocolVersion = Integer.toString(DeviceHelper.PROTOCOL_VERSION);
|
||||
serviceInfo.setAttribute("id", deviceId);
|
||||
serviceInfo.setAttribute("name", deviceName);
|
||||
serviceInfo.setAttribute("type", deviceType);
|
||||
@@ -216,6 +216,9 @@ public class MdnsDiscovery {
|
||||
|
||||
// Let the LanLinkProvider handle the connection
|
||||
InetAddress remoteAddress = serviceInfo.getHost();
|
||||
// TODO: In protocol version 8 we should be able to call "identityPacketReceived"
|
||||
// here, since we already have all the info we need to start a connection
|
||||
// and the remaining identity info will be exchanged later.
|
||||
lanLinkProvider.sendUdpIdentityPacket(Collections.singletonList(remoteAddress), null);
|
||||
}
|
||||
};
|
||||
|
@@ -50,7 +50,7 @@ import org.kde.kdeconnect_tp.R
|
||||
class BackgroundService : Service() {
|
||||
private lateinit var applicationInstance: KdeConnect
|
||||
|
||||
private val linkProviders = ArrayList<BaseLinkProvider>()
|
||||
private val linkProviders = mutableListOf<BaseLinkProvider>()
|
||||
|
||||
private val connectedToNonCellularNetwork = MutableLiveData<Boolean>()
|
||||
/** Indicates whether device is connected over wifi / usb / bluetooth / (anything other than cellular) */
|
||||
@@ -148,8 +148,8 @@ class BackgroundService : Service() {
|
||||
private fun createForegroundNotification(): Notification {
|
||||
// Why is this needed: https://developer.android.com/guide/components/services#Foreground
|
||||
|
||||
val connectedDevices = ArrayList<String>()
|
||||
val connectedDeviceIds = ArrayList<String>()
|
||||
val connectedDevices = mutableListOf<String>()
|
||||
val connectedDeviceIds = mutableListOf<String>()
|
||||
for (device in applicationInstance.devices.values) {
|
||||
if (device.isReachable && device.isPaired) {
|
||||
connectedDeviceIds.add(device.deviceId)
|
||||
|
@@ -33,7 +33,6 @@ import org.kde.kdeconnect.DeviceStats.countReceived
|
||||
import org.kde.kdeconnect.DeviceStats.countSent
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper
|
||||
import org.kde.kdeconnect.Helpers.NotificationHelper
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
|
||||
import org.kde.kdeconnect.PairingHandler.PairingCallback
|
||||
import org.kde.kdeconnect.Plugins.Plugin
|
||||
import org.kde.kdeconnect.Plugins.Plugin.Companion.getPluginKey
|
||||
@@ -46,6 +45,7 @@ import java.util.Vector
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import androidx.core.content.edit
|
||||
|
||||
class Device : PacketReceiver {
|
||||
|
||||
@@ -150,25 +150,27 @@ class Device : PacketReceiver {
|
||||
val deviceType: DeviceType
|
||||
get() = deviceInfo.type
|
||||
|
||||
val protocolVersion: Int
|
||||
get() = deviceInfo.protocolVersion
|
||||
|
||||
val deviceId: String
|
||||
get() = deviceInfo.id
|
||||
|
||||
val certificate: Certificate
|
||||
get() = deviceInfo.certificate
|
||||
|
||||
val verificationKey: String?
|
||||
get() = pairingHandler.verificationKey()
|
||||
|
||||
// Returns 0 if the version matches, < 0 if it is older or > 0 if it is newer
|
||||
fun compareProtocolVersion(): Int =
|
||||
deviceInfo.protocolVersion - DeviceHelper.ProtocolVersion
|
||||
|
||||
deviceInfo.protocolVersion - DeviceHelper.PROTOCOL_VERSION
|
||||
|
||||
val isPaired: Boolean
|
||||
get() = pairingHandler.state == PairingHandler.PairState.Paired
|
||||
|
||||
val isPairRequested: Boolean
|
||||
get() = pairingHandler.state == PairingHandler.PairState.Requested
|
||||
|
||||
val isPairRequestedByPeer: Boolean
|
||||
get() = pairingHandler.state == PairingHandler.PairState.RequestedByPeer
|
||||
val pairStatus : PairingHandler.PairState
|
||||
get() = pairingHandler.state
|
||||
|
||||
fun addPairingCallback(callback: PairingCallback) = pairingCallbacks.add(callback)
|
||||
|
||||
@@ -207,7 +209,7 @@ class Device : PacketReceiver {
|
||||
|
||||
// Store as trusted device
|
||||
val preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE)
|
||||
preferences.edit().putBoolean(deviceInfo.id, true).apply()
|
||||
preferences.edit { putBoolean(deviceInfo.id, true) }
|
||||
|
||||
try {
|
||||
reloadPluginsFromSettings()
|
||||
@@ -226,10 +228,10 @@ class Device : PacketReceiver {
|
||||
override fun unpaired() {
|
||||
Log.i("Device", "unpaired, removing from trusted devices list")
|
||||
val preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE)
|
||||
preferences.edit().remove(deviceInfo.id).apply()
|
||||
preferences.edit { remove(deviceInfo.id) }
|
||||
|
||||
val devicePreferences = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE)
|
||||
devicePreferences.edit().clear().apply()
|
||||
devicePreferences.edit { clear() }
|
||||
|
||||
pairingCallbacks.forEach(PairingCallback::unpaired)
|
||||
|
||||
@@ -286,8 +288,6 @@ class Device : PacketReceiver {
|
||||
|
||||
val notificationManager = ContextCompat.getSystemService(context, NotificationManager::class.java)!!
|
||||
|
||||
val verificationKey = SslHelper.getVerificationKey(SslHelper.certificate, deviceInfo.certificate)
|
||||
|
||||
val noti = NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)
|
||||
.setContentTitle(res.getString(R.string.pairing_request_from, name))
|
||||
.setContentText(res.getString(R.string.pairing_verification_code, verificationKey))
|
||||
@@ -358,10 +358,11 @@ class Device : PacketReceiver {
|
||||
|
||||
fun updateDeviceInfo(newDeviceInfo: DeviceInfo): Boolean {
|
||||
var hasChanges = false
|
||||
if (deviceInfo.name != newDeviceInfo.name || deviceInfo.type != newDeviceInfo.type) {
|
||||
if (deviceInfo.name != newDeviceInfo.name || deviceInfo.type != newDeviceInfo.type || deviceInfo.protocolVersion != newDeviceInfo.protocolVersion) {
|
||||
hasChanges = true
|
||||
deviceInfo.name = newDeviceInfo.name
|
||||
deviceInfo.type = newDeviceInfo.type
|
||||
deviceInfo.protocolVersion = newDeviceInfo.protocolVersion
|
||||
if (isPaired) {
|
||||
deviceInfo.saveInSettings(settings)
|
||||
}
|
||||
@@ -595,7 +596,7 @@ class Device : PacketReceiver {
|
||||
}
|
||||
|
||||
fun setPluginEnabled(pluginKey: String, value: Boolean) {
|
||||
settings.edit().putBoolean(pluginKey, value).apply()
|
||||
settings.edit { putBoolean(pluginKey, value) }
|
||||
reloadPluginsFromSettings()
|
||||
}
|
||||
|
||||
|
65
src/org/kde/kdeconnect/DeviceHost.kt
Normal file
65
src/org/kde/kdeconnect/DeviceHost.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
package org.kde.kdeconnect
|
||||
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper
|
||||
import java.net.InetAddress
|
||||
|
||||
class DeviceHost private constructor(private val host: String) {
|
||||
// Wrapper because Kotlin doesn't allow nested nullability
|
||||
data class PingResult(val latency: Long?)
|
||||
|
||||
/** The amount of milliseconds the ping request took or null it's in progress */
|
||||
var ping: PingResult? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Checks if the host can be reached over the network.
|
||||
* @param callback Callback for updating UI elements
|
||||
*/
|
||||
fun checkReachable(callback: () -> Unit) {
|
||||
ThreadHelper.execute {
|
||||
try {
|
||||
val address = InetAddress.getByName(this.host)
|
||||
val startTime = System.currentTimeMillis()
|
||||
val pingable = address.isReachable(PING_TIMEOUT)
|
||||
val delayMillis = System.currentTimeMillis() - startTime
|
||||
val pingResult = PingResult(if (pingable) delayMillis else null)
|
||||
ping = pingResult
|
||||
}
|
||||
catch (_: Exception) {
|
||||
ping = PingResult(null)
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
require(isValidDeviceHost(host)) { "Invalid host" }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return this.host
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Ping timeout */
|
||||
private const val PING_TIMEOUT = 3_000
|
||||
private val hostnameValidityPattern = Regex("^[0-9A-Za-z._-]+$")
|
||||
|
||||
@JvmStatic
|
||||
fun isValidDeviceHost(host: String): Boolean {
|
||||
return hostnameValidityPattern.matches(host)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun toDeviceHostOrNull(host: String): DeviceHost? {
|
||||
return if (isValidDeviceHost(host)) {
|
||||
DeviceHost(host)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val BROADCAST: DeviceHost = DeviceHost("255.255.255.255")
|
||||
}
|
||||
}
|
@@ -44,6 +44,7 @@ class DeviceInfo(
|
||||
putString("certificate", encodedCertificate)
|
||||
putString("deviceName", name)
|
||||
putString("deviceType", type.toString())
|
||||
putInt("protocolVersion", protocolVersion)
|
||||
apply()
|
||||
}
|
||||
} catch (e: CertificateEncodingException) {
|
||||
@@ -58,12 +59,12 @@ class DeviceInfo(
|
||||
*/
|
||||
fun toIdentityPacket(): NetworkPacket =
|
||||
NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY).also { np ->
|
||||
np.set("deviceId", id)
|
||||
np.set("deviceName", name)
|
||||
np.set("protocolVersion", protocolVersion)
|
||||
np.set("deviceType", type.toString())
|
||||
np.set("incomingCapabilities", incomingCapabilities!!)
|
||||
np.set("outgoingCapabilities", outgoingCapabilities!!)
|
||||
np["deviceId"] = id
|
||||
np["deviceName"] = name
|
||||
np["protocolVersion"] = protocolVersion
|
||||
np["deviceType"] = type.toString()
|
||||
np["incomingCapabilities"] = incomingCapabilities!!
|
||||
np["outgoingCapabilities"] = outgoingCapabilities!!
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -79,7 +80,8 @@ class DeviceInfo(
|
||||
id = deviceId,
|
||||
name = getString("deviceName", "unknown")!!,
|
||||
type = DeviceType.fromString(getString("deviceType", "desktop")!!),
|
||||
certificate = SslHelper.getDeviceCertificate(context, deviceId)
|
||||
certificate = SslHelper.getDeviceCertificate(context, deviceId),
|
||||
protocolVersion = getInt("protocolVersion", 0),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,8 +107,13 @@ class DeviceInfo(
|
||||
fun isValidIdentityPacket(identityPacket: NetworkPacket): Boolean = with(identityPacket) {
|
||||
type == NetworkPacket.PACKET_TYPE_IDENTITY &&
|
||||
DeviceHelper.filterName(getString("deviceName", "")).isNotBlank() &&
|
||||
getString("deviceId", "").isNotBlank()
|
||||
isValidDeviceId(getString("deviceId", ""))
|
||||
}
|
||||
|
||||
private val DEVICE_ID_REGEX = "^[a-zA-Z0-9_-]{32,38}$".toRegex()
|
||||
|
||||
@JvmStatic
|
||||
fun isValidDeviceId(deviceId: String): Boolean = deviceId.matches(DEVICE_ID_REGEX)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -115,7 +115,7 @@ object DeviceStats {
|
||||
val entry = iterator.next()
|
||||
val events = entry.value
|
||||
|
||||
var index = Collections.binarySearch(events, cutoutTimestamp)
|
||||
var index = events.binarySearch(cutoutTimestamp)
|
||||
if (index < 0) {
|
||||
index = -(index + 1) // Convert the negative index to insertion point
|
||||
}
|
||||
|
@@ -91,7 +91,7 @@ public final class CollectionsBackport {
|
||||
}
|
||||
|
||||
static boolean eq(Object o1, Object o2) {
|
||||
return o1 == null ? o2 == null : o1.equals(o2);
|
||||
return Objects.equals(o1, o2);
|
||||
}
|
||||
|
||||
static class UnmodifiableNavigableSetBackport<E>
|
||||
|
@@ -214,7 +214,7 @@ public class ContactsHelper {
|
||||
|
||||
Map<uID, Map<String, String>> databaseValue = accessContactsDatabase(context, projection, selection, selectionArgs, null);
|
||||
|
||||
if (databaseValue.size() == 0) {
|
||||
if (databaseValue.isEmpty()) {
|
||||
throw new ContactNotFoundException("Querying for contact with id " + contactID + " returned no results.");
|
||||
}
|
||||
|
||||
|
27
src/org/kde/kdeconnect/Helpers/CreateFileResultContract.kt
Normal file
27
src/org/kde/kdeconnect/Helpers/CreateFileResultContract.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package org.kde.kdeconnect.Helpers
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
|
||||
data class CreateFileParams(
|
||||
val fileMimeType: String,
|
||||
val suggestedFileName: String,
|
||||
)
|
||||
|
||||
class CreateFileResultContract : ActivityResultContract<CreateFileParams, Uri?>() {
|
||||
|
||||
override fun createIntent(context: Context, input: CreateFileParams): Intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
setTypeAndNormalize(input.fileMimeType)
|
||||
putExtra(Intent.EXTRA_TITLE, input.suggestedFileName)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Uri? = when (resultCode) {
|
||||
Activity.RESULT_OK -> intent?.data
|
||||
else -> null
|
||||
}
|
||||
}
|
@@ -11,7 +11,6 @@ import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import com.univocity.parsers.common.TextParsingException
|
||||
import com.univocity.parsers.csv.CsvParser
|
||||
@@ -26,9 +25,10 @@ import java.io.InputStreamReader
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.UUID
|
||||
import androidx.core.content.edit
|
||||
|
||||
object DeviceHelper {
|
||||
const val ProtocolVersion = 7
|
||||
const val PROTOCOL_VERSION = 8
|
||||
|
||||
const val KEY_DEVICE_NAME_PREFERENCE = "device_name_preference"
|
||||
private const val KEY_DEVICE_NAME_FETCHED_FROM_THE_INTERNET = "device_name_downloaded_preference"
|
||||
@@ -85,7 +85,7 @@ object DeviceHelper {
|
||||
|
||||
// If we get here we managed to download the file. Mark that as done so we don't try again even if we don't end up finding a name.
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
preferences.edit().putBoolean(KEY_DEVICE_NAME_FETCHED_FROM_THE_INTERNET, true).apply()
|
||||
preferences.edit { putBoolean(KEY_DEVICE_NAME_FETCHED_FROM_THE_INTERNET, true) }
|
||||
|
||||
BufferedReader(
|
||||
InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_16)
|
||||
@@ -124,26 +124,17 @@ object DeviceHelper {
|
||||
fun setDeviceName(context: Context, name: String) {
|
||||
val filteredName = filterName(name)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
preferences.edit().putString(KEY_DEVICE_NAME_PREFERENCE, filteredName).apply()
|
||||
preferences.edit { putString(KEY_DEVICE_NAME_PREFERENCE, filteredName) }
|
||||
}
|
||||
|
||||
fun initializeDeviceId(context: Context) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val preferenceKeys: Set<String> = preferences.all.keys
|
||||
if (preferenceKeys.contains(KEY_DEVICE_ID_PREFERENCE)) {
|
||||
val deviceId = preferences.getString(KEY_DEVICE_ID_PREFERENCE, "")!!
|
||||
if (DeviceInfo.isValidDeviceId(deviceId)) {
|
||||
return // We already have an ID
|
||||
}
|
||||
@SuppressLint("HardwareIds")
|
||||
val deviceName = if (preferenceKeys.isEmpty()) {
|
||||
// For new installations, use random IDs
|
||||
Log.i("DeviceHelper","No device ID found and this looks like a new installation, creating a random ID")
|
||||
UUID.randomUUID().toString().replace('-', '_')
|
||||
} else {
|
||||
// Use the ANDROID_ID as device ID for existing installations, for backwards compatibility
|
||||
Log.i("DeviceHelper", "No device ID found but this seems an existing installation, using the Android ID")
|
||||
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
||||
}
|
||||
preferences.edit().putString(KEY_DEVICE_ID_PREFERENCE, deviceName).apply()
|
||||
val deviceName = UUID.randomUUID().toString().replace("-", "")
|
||||
preferences.edit { putString(KEY_DEVICE_ID_PREFERENCE, deviceName) }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -159,12 +150,12 @@ object DeviceHelper {
|
||||
SslHelper.certificate,
|
||||
getDeviceName(context),
|
||||
deviceType,
|
||||
ProtocolVersion,
|
||||
PROTOCOL_VERSION,
|
||||
PluginFactory.incomingCapabilities,
|
||||
PluginFactory.outgoingCapabilities
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun filterName(input: String): String = input.replace(NAME_INVALID_CHARACTERS_REGEX, "").take(MAX_DEVICE_NAME_LENGTH)
|
||||
fun filterName(input: String): String = input.replace(NAME_INVALID_CHARACTERS_REGEX, "").trim().take(MAX_DEVICE_NAME_LENGTH)
|
||||
}
|
||||
|
@@ -97,7 +97,7 @@ object FilesHelper {
|
||||
|
||||
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,)
|
||||
val proj = arrayOf(OpenableColumns.SIZE, OpenableColumns.DISPLAY_NAME)
|
||||
|
||||
try {
|
||||
contentResolver.query(uri, proj, null, null, null).use { cursor ->
|
||||
|
@@ -11,7 +11,6 @@ import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.database.sqlite.SQLiteException
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.ThumbnailUtils
|
||||
import android.net.Uri
|
||||
@@ -23,6 +22,8 @@ import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import android.util.Pair
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.net.toUri
|
||||
import com.google.android.mms.pdu_alt.MultimediaMessagePdu
|
||||
import com.google.android.mms.pdu_alt.PduPersister
|
||||
import com.google.android.mms.util_alt.PduCache
|
||||
@@ -37,13 +38,11 @@ import org.kde.kdeconnect.Helpers.TelephonyHelper.LocalPhoneNumber
|
||||
import org.kde.kdeconnect.Plugins.SMSPlugin.MimeType
|
||||
import org.kde.kdeconnect.Plugins.SMSPlugin.SmsMmsUtils
|
||||
import java.io.IOException
|
||||
import java.util.Arrays
|
||||
import java.util.Objects
|
||||
import java.util.SortedMap
|
||||
import java.util.TreeMap
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.stream.Collectors
|
||||
import kotlin.text.Charsets.UTF_8
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@@ -52,8 +51,7 @@ object SMSHelper {
|
||||
private const val THUMBNAIL_WIDTH = 100
|
||||
|
||||
// The constant Telephony.Mms.Part.CONTENT_URI was added in API 29
|
||||
@JvmField
|
||||
val mMSPartUri : Uri = Uri.parse("content://mms/part/")
|
||||
val mMSPartUri : Uri = "content://mms/part/".toUri()
|
||||
|
||||
/**
|
||||
* Get the base address for all message conversations
|
||||
@@ -68,14 +66,14 @@ object SMSHelper {
|
||||
if ("Samsung".equals(Build.MANUFACTURER, ignoreCase = true)) {
|
||||
Log.i("SMSHelper", "This appears to be a Samsung device. This may cause some features to not work properly.")
|
||||
}
|
||||
return Uri.parse("content://mms-sms/conversations?simple=true")
|
||||
return "content://mms-sms/conversations?simple=true".toUri()
|
||||
}
|
||||
|
||||
private fun getCompleteConversationsUri(): Uri {
|
||||
// This glorious - but completely undocumented - content URI gives us all messages, both MMS and SMS,
|
||||
// in all conversations
|
||||
// See https://stackoverflow.com/a/36439630/3723163
|
||||
return Uri.parse("content://mms-sms/complete-conversations")
|
||||
return "content://mms-sms/complete-conversations".toUri()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,10 +92,7 @@ object SMSHelper {
|
||||
* @param context android.content.Context running the request
|
||||
* @return Timestamp of the oldest known message.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getNewestMessageTimestamp(
|
||||
context: Context
|
||||
): Long {
|
||||
fun getNewestMessageTimestamp(context: Context): Long {
|
||||
var oldestMessageTimestamp = Long.MIN_VALUE
|
||||
val newestMessage = getMessagesInRange(context, null, Long.MAX_VALUE, 1L, true)
|
||||
// There should only be one, but in case for some reason there are more, take the latest
|
||||
@@ -117,12 +112,7 @@ object SMSHelper {
|
||||
* @param numberToGet Number of messages to return. Pass null for "all"
|
||||
* @return List of all messages in the thread
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getMessagesInThread(
|
||||
context: Context,
|
||||
threadID: ThreadID,
|
||||
numberToGet: Long?
|
||||
): List<Message> {
|
||||
fun getMessagesInThread(context: Context, threadID: ThreadID, numberToGet: Long?): List<Message> {
|
||||
return getMessagesInRange(context, threadID, Long.MAX_VALUE, numberToGet, true)
|
||||
}
|
||||
|
||||
@@ -136,7 +126,6 @@ object SMSHelper {
|
||||
* @param getMessagesOlderStartTime If true, get messages with timestamps before the startTimestamp. If false, get newer messages
|
||||
* @return Some messages in the requested conversation
|
||||
*/
|
||||
@JvmStatic
|
||||
@SuppressLint("NewApi")
|
||||
fun getMessagesInRange(
|
||||
context: Context,
|
||||
@@ -441,10 +430,7 @@ object SMSHelper {
|
||||
* @param context android.content.Context running the request
|
||||
* @return Non-blocking iterable of the first message in each conversation
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getConversations(
|
||||
context: Context
|
||||
): Sequence<Message> {
|
||||
fun getConversations(context: Context): Sequence<Message> {
|
||||
val uri = getConversationUri()
|
||||
|
||||
// Used to avoid spewing logs in case there is an overall problem with fetching thread IDs
|
||||
@@ -507,14 +493,10 @@ object SMSHelper {
|
||||
}
|
||||
threadTimestampPair.add(Pair(threadID, messageDate))
|
||||
}
|
||||
threadIds = threadTimestampPair.stream()
|
||||
.sorted { left: Pair<ThreadID, Long>, right: Pair<ThreadID, Long> ->
|
||||
right.second.compareTo(
|
||||
left.second
|
||||
)
|
||||
} // Sort most-recent to least-recent (largest to smallest)
|
||||
threadIds = threadTimestampPair
|
||||
// Sort most-recent to least-recent (largest to smallest)
|
||||
.sortedWith { left: Pair<ThreadID, Long>, right: Pair<ThreadID, Long> -> right.second.compareTo(left.second) }
|
||||
.map { threadTimestampPairElement: Pair<ThreadID, Long> -> threadTimestampPairElement.first }
|
||||
.collect(Collectors.toList())
|
||||
}
|
||||
|
||||
// Step 2: Get the actual message object from each thread ID
|
||||
@@ -538,25 +520,16 @@ object SMSHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private fun addEventFlag(
|
||||
oldEvent: Int,
|
||||
eventFlag: Int
|
||||
): Int {
|
||||
private fun addEventFlag(oldEvent: Int, eventFlag: Int): Int {
|
||||
return oldEvent or eventFlag
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all parts of an SMS into a Message
|
||||
*/
|
||||
private fun parseSMS(
|
||||
context: Context,
|
||||
messageInfo: Map<String, String?>
|
||||
): Message {
|
||||
var event = Message.EVENT_UNKNOWN
|
||||
event = addEventFlag(event, Message.EVENT_TEXT_MESSAGE)
|
||||
val address = listOf(
|
||||
Address(context, messageInfo[Telephony.Sms.ADDRESS]!!)
|
||||
)
|
||||
private fun parseSMS(context: Context, messageInfo: Map<String, String?>): Message {
|
||||
val event = addEventFlag(Message.EVENT_UNKNOWN, Message.EVENT_TEXT_MESSAGE)
|
||||
val address = listOf(Address(context, messageInfo[Telephony.Sms.ADDRESS]!!))
|
||||
val maybeBody = messageInfo.getOrDefault(Message.BODY, "")
|
||||
val body = maybeBody ?: ""
|
||||
val date = NumberUtils.toLong(messageInfo.getOrDefault(Message.DATE, null))
|
||||
@@ -569,28 +542,14 @@ object SMSHelper {
|
||||
)
|
||||
)
|
||||
val uID = NumberUtils.toLong(messageInfo.getOrDefault(Message.U_ID, null))
|
||||
val subscriptionID =
|
||||
NumberUtils.toInt(messageInfo.getOrDefault(Message.SUBSCRIPTION_ID, null))
|
||||
val subscriptionID = NumberUtils.toInt(messageInfo.getOrDefault(Message.SUBSCRIPTION_ID, null))
|
||||
|
||||
// Examine all the required SMS columns and emit a log if something seems amiss
|
||||
val anyNulls = Arrays.stream(
|
||||
arrayOf(
|
||||
Telephony.Sms.ADDRESS,
|
||||
Message.BODY,
|
||||
Message.DATE,
|
||||
Message.TYPE,
|
||||
Message.READ,
|
||||
Message.THREAD_ID,
|
||||
Message.U_ID
|
||||
)
|
||||
)
|
||||
val anyNulls = arrayOf(Telephony.Sms.ADDRESS, Message.BODY, Message.DATE, Message.TYPE, Message.READ, Message.THREAD_ID, Message.U_ID)
|
||||
.map { key: String -> messageInfo.getOrDefault(key, null) }
|
||||
.anyMatch { obj: String? -> Objects.isNull(obj) }
|
||||
.any { obj: String? -> Objects.isNull(obj) }
|
||||
if (anyNulls) {
|
||||
Log.e(
|
||||
"parseSMS",
|
||||
"Some fields were invalid. This indicates either a corrupted SMS database or an unsupported device."
|
||||
)
|
||||
Log.e("parseSMS", "Some fields were invalid. This indicates either a corrupted SMS database or an unsupported device.")
|
||||
}
|
||||
return Message(
|
||||
address,
|
||||
@@ -610,11 +569,7 @@ object SMSHelper {
|
||||
* Parse all parts of the MMS message into a message
|
||||
* Original implementation from https://stackoverflow.com/a/6446831/3723163
|
||||
*/
|
||||
private fun parseMMS(
|
||||
context: Context,
|
||||
messageInfo: Map<String, String?>,
|
||||
userPhoneNumbers: List<LocalPhoneNumber>
|
||||
): Message {
|
||||
private fun parseMMS(context: Context, messageInfo: Map<String, String?>, userPhoneNumbers: List<LocalPhoneNumber>): Message {
|
||||
var event = Message.EVENT_UNKNOWN
|
||||
var body = ""
|
||||
val read = NumberUtils.toInt(messageInfo[Message.READ])
|
||||
@@ -695,12 +650,7 @@ object SMSHelper {
|
||||
)
|
||||
val videoThumbnail = retriever.frameAtTime
|
||||
val encodedThumbnail = SmsMmsUtils.bitMapToBase64(
|
||||
Bitmap.createScaledBitmap(
|
||||
videoThumbnail!!,
|
||||
THUMBNAIL_WIDTH,
|
||||
THUMBNAIL_HEIGHT,
|
||||
true
|
||||
)
|
||||
videoThumbnail!!.scale(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
|
||||
)
|
||||
attachments.add(
|
||||
Attachment(
|
||||
@@ -747,8 +697,8 @@ object SMSHelper {
|
||||
val to = SmsMmsUtils.getMmsTo(context, msg)
|
||||
val addresses: MutableList<Address> = ArrayList()
|
||||
if (from != null) {
|
||||
val isLocalPhoneNumber = userPhoneNumbers.stream()
|
||||
.anyMatch { localPhoneNumber: LocalPhoneNumber ->
|
||||
val isLocalPhoneNumber = userPhoneNumbers
|
||||
.any { localPhoneNumber: LocalPhoneNumber ->
|
||||
localPhoneNumber.isMatchingPhoneNumber(from.getAddress())
|
||||
}
|
||||
if (!isLocalPhoneNumber && from.toString() != "insert-address-token") {
|
||||
@@ -757,8 +707,8 @@ object SMSHelper {
|
||||
}
|
||||
if (to != null) {
|
||||
for (toAddress in to) {
|
||||
val isLocalPhoneNumber = userPhoneNumbers.stream()
|
||||
.anyMatch { localPhoneNumber: LocalPhoneNumber ->
|
||||
val isLocalPhoneNumber = userPhoneNumbers
|
||||
.any { localPhoneNumber: LocalPhoneNumber ->
|
||||
localPhoneNumber.isMatchingPhoneNumber(toAddress.getAddress())
|
||||
}
|
||||
if (!isLocalPhoneNumber && toAddress.toString() != "insert-address-token") {
|
||||
@@ -837,16 +787,8 @@ object SMSHelper {
|
||||
*
|
||||
* @param observer ContentObserver to alert on Message changes
|
||||
*/
|
||||
@JvmStatic
|
||||
fun registerObserver(
|
||||
observer: ContentObserver,
|
||||
context: Context
|
||||
) {
|
||||
context.contentResolver.registerContentObserver(
|
||||
getConversationUri(),
|
||||
true,
|
||||
observer
|
||||
)
|
||||
fun registerObserver(observer: ContentObserver, context: Context) {
|
||||
context.contentResolver.registerContentObserver(getConversationUri(), true, observer)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -862,10 +804,7 @@ object SMSHelper {
|
||||
* ...
|
||||
* ]
|
||||
</String></String></String></Attachment> */
|
||||
@JvmStatic
|
||||
fun jsonArrayToAttachmentsList(
|
||||
jsonArray: JSONArray?
|
||||
): List<Attachment> {
|
||||
fun jsonArrayToAttachmentsList(jsonArray: JSONArray?): List<Attachment> {
|
||||
if (jsonArray == null) {
|
||||
return emptyList()
|
||||
}
|
||||
@@ -887,11 +826,7 @@ object SMSHelper {
|
||||
/**
|
||||
* converts a given JSONArray into List<Address>
|
||||
</Address> */
|
||||
@JvmStatic
|
||||
fun jsonArrayToAddressList(context: Context, jsonArray: JSONArray?): List<Address>? {
|
||||
if (jsonArray == null) {
|
||||
return null
|
||||
}
|
||||
fun jsonArrayToAddressList(context: Context, jsonArray: JSONArray): List<Address> {
|
||||
val addresses: MutableList<Address> = ArrayList()
|
||||
try {
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
@@ -909,13 +844,9 @@ object SMSHelper {
|
||||
* Represent an ID used to uniquely identify a message thread
|
||||
*/
|
||||
class ThreadID(val threadID: Long) {
|
||||
override fun toString(): String {
|
||||
return threadID.toString()
|
||||
}
|
||||
override fun toString(): String = threadID.toString()
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return java.lang.Long.hashCode(threadID)
|
||||
}
|
||||
override fun hashCode(): Int = java.lang.Long.hashCode(threadID)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other!!.javaClass.isAssignableFrom(ThreadID::class.java) && (other as ThreadID?)!!.threadID == threadID
|
||||
@@ -933,9 +864,9 @@ object SMSHelper {
|
||||
|
||||
class Attachment(
|
||||
private val partID: Long,
|
||||
@JvmField val mimeType: String,
|
||||
@JvmField val base64EncodedFile: String?,
|
||||
@JvmField val uniqueIdentifier: String
|
||||
val mimeType: String,
|
||||
val base64EncodedFile: String?,
|
||||
val uniqueIdentifier: String
|
||||
) {
|
||||
|
||||
@Throws(JSONException::class)
|
||||
@@ -1020,7 +951,7 @@ object SMSHelper {
|
||||
class Message internal constructor(
|
||||
private val addresses: List<Address>,
|
||||
val body: String,
|
||||
@JvmField val date: Long,
|
||||
val date: Long,
|
||||
val type: Int,
|
||||
val read: Int,
|
||||
private val threadID: ThreadID,
|
||||
@@ -1149,7 +1080,7 @@ object SMSHelper {
|
||||
* Since this means a thread has to be spawned, this method might block until that thread is
|
||||
* ready to serve requests
|
||||
*/
|
||||
@JvmStatic
|
||||
@JvmStatic // required for ConnectivityReportPlugin.java
|
||||
fun getLooper(): Looper? {
|
||||
if (singleton == null) {
|
||||
looperReadyLock.lock()
|
||||
|
@@ -6,6 +6,7 @@
|
||||
|
||||
package org.kde.kdeconnect.Helpers.SecurityHelpers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
@@ -23,7 +24,6 @@ import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
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;
|
||||
@@ -62,7 +62,7 @@ import javax.security.auth.x500.X500Principal;
|
||||
public class SslHelper {
|
||||
|
||||
public static Certificate certificate; //my device's certificate
|
||||
private static CertificateFactory factory;
|
||||
private static final CertificateFactory factory;
|
||||
static {
|
||||
try {
|
||||
factory = CertificateFactory.getInstance("X.509");
|
||||
@@ -71,20 +71,14 @@ public class SslHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private final static TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
|
||||
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
@SuppressLint({"CustomX509TrustManager", "TrustAllX509TrustManager"})
|
||||
private final static TrustManager[] trustAllCerts = new TrustManager[] {
|
||||
new X509TrustManager() {
|
||||
private final X509Certificate[] issuers = new X509Certificate[0];
|
||||
@Override public X509Certificate[] getAcceptedIssuers() { return issuers; }
|
||||
@Override public void checkClientTrusted(X509Certificate[] certs, String authType) { }
|
||||
@Override public void checkServerTrusted(X509Certificate[] certs, String authType) { }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
public static void initialiseCertificate(Context context) {
|
||||
@@ -287,31 +281,4 @@ public class SslHelper {
|
||||
return IETFUtils.valueToString(rdn.getFirst().getValue());
|
||||
}
|
||||
|
||||
public static String getVerificationKey(Certificate certificateA, Certificate certificateB) {
|
||||
try {
|
||||
byte[] a = certificateA.getPublicKey().getEncoded();
|
||||
byte[] b = certificateB.getPublicKey().getEncoded();
|
||||
|
||||
if (Arrays.compareUnsigned(a, b) < 0) {
|
||||
// Swap them so on both devices they are in the same order
|
||||
byte[] aux = a;
|
||||
a = b;
|
||||
b = aux;
|
||||
}
|
||||
|
||||
byte[] concat = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, concat, 0, a.length);
|
||||
System.arraycopy(b, 0, concat, a.length, b.length);
|
||||
|
||||
byte[] hash = MessageDigest.getInstance("SHA-256").digest(concat);
|
||||
Formatter formatter = new Formatter();
|
||||
for (byte value : hash) {
|
||||
formatter.format("%02x", value);
|
||||
}
|
||||
return formatter.toString().substring(0,8).toUpperCase(Locale.ROOT);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@
|
||||
package org.kde.kdeconnect.Helpers
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
|
||||
object StorageHelper {
|
||||
fun getDisplayName(treeUri: Uri): String {
|
||||
|
@@ -13,6 +13,7 @@ import android.net.wifi.WifiManager
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
|
||||
class TrustedNetworkHelper(private val context: Context) {
|
||||
|
||||
@@ -22,19 +23,19 @@ class TrustedNetworkHelper(private val context: Context) {
|
||||
return serializedNetworks.split(NETWORK_SSID_DELIMITER, "#_#" /* TODO remove old delimiter in 2025 */).filter { it.isNotEmpty() }
|
||||
}
|
||||
set(value) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString(KEY_CUSTOM_TRUSTED_NETWORKS, value.joinToString(NETWORK_SSID_DELIMITER))
|
||||
.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putString(
|
||||
KEY_CUSTOM_TRUSTED_NETWORKS,
|
||||
value.joinToString(NETWORK_SSID_DELIMITER)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var allNetworksAllowed: Boolean
|
||||
get() = !hasPermissions || PreferenceManager.getDefaultSharedPreferences(context).getBoolean(KEY_CUSTOM_TRUST_ALL_NETWORKS, true)
|
||||
set(value) = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putBoolean(KEY_CUSTOM_TRUST_ALL_NETWORKS, value)
|
||||
.apply()
|
||||
set(value) = PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putBoolean(KEY_CUSTOM_TRUST_ALL_NETWORKS, value)
|
||||
}
|
||||
|
||||
val hasPermissions: Boolean
|
||||
get() = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||
|
@@ -13,6 +13,9 @@ object VideoUrlsHelper {
|
||||
private const val MINUTES_IN_HOUR = 60
|
||||
private const val SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR
|
||||
|
||||
/** PeerTube uses a Flickr Base58 encoded short UUID (alphanumeric, but 0, O, I, and l are excluded) with a length of 22 characters **/
|
||||
private val peerTubePathPattern = Regex("^/w/[1-9a-km-zA-HJ-NP-Z]{22}(\\?.+)?$")
|
||||
|
||||
@Throws(MalformedURLException::class)
|
||||
fun formatUriWithSeek(address: String, position: Long): URL {
|
||||
val positionSeconds = position / 1000 // milliseconds to seconds
|
||||
@@ -33,7 +36,10 @@ object VideoUrlsHelper {
|
||||
url.editParameter("start", Regex("\\d+")) { "$positionSeconds" }
|
||||
}
|
||||
host.contains("twitch.tv") -> {
|
||||
url.editParameter("t", Regex("(\\d+[hH])?(\\d+[mM])?\\d+[sS]")) { positionSeconds.formatTimestampHMS() }
|
||||
url.editParameter("t", Regex("(\\d+[hH])?(\\d+[mM])?\\d+[sS]")) { formatTimestampHMS(positionSeconds) }
|
||||
}
|
||||
url.path.matches(peerTubePathPattern) -> {
|
||||
url.editParameter("start", Regex("(\\d+[hH])?(\\d+[mM])?\\d+[sS]")) { formatTimestampHMS(positionSeconds) }
|
||||
}
|
||||
else -> url
|
||||
}
|
||||
@@ -62,13 +68,16 @@ object VideoUrlsHelper {
|
||||
return URL("${urlBase}?${modifiedUrlQuery}") // -> "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=14"
|
||||
}
|
||||
|
||||
/** Formats timestamp to e.g. 01h02m34s */
|
||||
private fun Long.formatTimestampHMS(): String {
|
||||
if (this == 0L) return "0s"
|
||||
/**
|
||||
* @param timestamp in seconds
|
||||
* @return timestamp formatted as e.g. "01h02m34s"
|
||||
* */
|
||||
private fun formatTimestampHMS(timestamp: Long): String {
|
||||
if (timestamp == 0L) return "0s"
|
||||
|
||||
val seconds: Long = this % SECONDS_IN_MINUTE
|
||||
val minutes: Long = (this / SECONDS_IN_MINUTE) % MINUTES_IN_HOUR
|
||||
val hours: Long = this / SECONDS_IN_HOUR
|
||||
val seconds: Long = timestamp % SECONDS_IN_MINUTE
|
||||
val minutes: Long = (timestamp / SECONDS_IN_MINUTE) % MINUTES_IN_HOUR
|
||||
val hours: Long = timestamp / SECONDS_IN_HOUR
|
||||
|
||||
fun pad(s: String) = s.padStart(3, '0')
|
||||
val hoursText = if (hours > 0) pad("${hours}h") else ""
|
||||
|
25
src/org/kde/kdeconnect/Helpers/WindowHelper.kt
Normal file
25
src/org/kde/kdeconnect/Helpers/WindowHelper.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Mash Kyrielight <fiepi@live.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.view.View
|
||||
import org.kde.kdeconnect.extensions.setupBottomMargin
|
||||
import org.kde.kdeconnect.extensions.setupBottomPadding
|
||||
|
||||
object WindowHelper {
|
||||
|
||||
// for java only
|
||||
@JvmStatic
|
||||
fun setupBottomPadding(view: View) {
|
||||
view.setupBottomPadding()
|
||||
}
|
||||
|
||||
// for java only
|
||||
@JvmStatic
|
||||
fun setupBottomMargin(view: View) {
|
||||
view.setupBottomMargin()
|
||||
}
|
||||
}
|
@@ -25,6 +25,7 @@ import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Date
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import androidx.core.content.edit
|
||||
|
||||
/*
|
||||
* This class holds all the active devices and makes them accessible from every other class.
|
||||
@@ -116,7 +117,7 @@ class KdeConnect : Application() {
|
||||
"KdeConnect",
|
||||
"Couldn't load the certificate for a remembered device. Removing from trusted list.", e
|
||||
)
|
||||
preferences.edit().remove(it).apply()
|
||||
preferences.edit { remove(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,7 +128,7 @@ class KdeConnect : Application() {
|
||||
trustedDevices.filter { preferences.getBoolean(it, false) }
|
||||
.forEach {
|
||||
Log.d("KdeConnect", "Removing devices: $it")
|
||||
preferences.edit().remove(it).apply()
|
||||
preferences.edit { remove(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,19 +11,15 @@ import org.json.JSONObject
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.RuntimeException
|
||||
import java.net.Socket
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
class NetworkPacket private constructor(
|
||||
val id: Long,
|
||||
val type: String,
|
||||
private val mBody: JSONObject,
|
||||
var payload: Payload?,
|
||||
var payloadTransferInfo: JSONObject,
|
||||
) {
|
||||
constructor(type: String) : this(
|
||||
id = System.currentTimeMillis(),
|
||||
type = type,
|
||||
mBody = JSONObject(),
|
||||
payload = null,
|
||||
@@ -209,7 +205,7 @@ class NetworkPacket private constructor(
|
||||
@Throws(JSONException::class)
|
||||
fun serialize(): String {
|
||||
val jo = JSONObject()
|
||||
jo.put("id", id)
|
||||
jo.put("id", System.currentTimeMillis())
|
||||
jo.put("type", type)
|
||||
jo.put("body", mBody)
|
||||
if (hasPayload()) {
|
||||
@@ -285,14 +281,13 @@ class NetworkPacket private constructor(
|
||||
@Throws(JSONException::class)
|
||||
fun unserialize(s: String): NetworkPacket {
|
||||
val jo = JSONObject(s)
|
||||
val id = jo.getLong("id")
|
||||
val type = jo.getString("type")
|
||||
val mBody = jo.getJSONObject("body")
|
||||
|
||||
val hasPayload = jo.has("payloadSize")
|
||||
val payloadTransferInfo = if (hasPayload) jo.getJSONObject("payloadTransferInfo") else JSONObject()
|
||||
val payload = if (hasPayload) Payload(jo.getLong("payloadSize")) else null
|
||||
return NetworkPacket(id, type, mBody, payload, payloadTransferInfo)
|
||||
return NetworkPacket(type, mBody, payload, payloadTransferInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,13 @@ package org.kde.kdeconnect
|
||||
import android.util.Log
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import kotlinx.coroutines.*
|
||||
import org.bouncycastle.util.Arrays
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
|
||||
import org.kde.kdeconnect_tp.R
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.Certificate
|
||||
import java.util.Formatter
|
||||
import kotlin.math.abs
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class PairingHandler(private val device: Device, private val callback: PairingCallback, var state: PairState) {
|
||||
@@ -31,6 +37,7 @@ class PairingHandler(private val device: Device, private val callback: PairingCa
|
||||
|
||||
private val pairingJob = SupervisorJob()
|
||||
private val pairingScope = CoroutineScope(Dispatchers.IO + pairingJob)
|
||||
private var pairingTimestamp = 0L
|
||||
|
||||
fun packetReceived(np: NetworkPacket) {
|
||||
cancelTimer()
|
||||
@@ -50,10 +57,26 @@ class PairingHandler(private val device: Device, private val callback: PairingCa
|
||||
Log.w("PairingHandler", "Received pairing request from a device we already trusted.")
|
||||
// It would be nice to auto-accept the pairing request here, but since the pairing accept and pairing request
|
||||
// messages are identical, this could create an infinite loop if both devices are "accepting" each other pairs.
|
||||
// Instead, unpair and handle as if "NotPaired".
|
||||
// Instead, unpair and handle as if "NotPaired". TODO: No longer true in protocol version 8
|
||||
state = PairState.NotPaired
|
||||
callback.unpaired()
|
||||
}
|
||||
|
||||
if (device.protocolVersion >= 8) {
|
||||
pairingTimestamp = np.getLong("timestamp", -1L)
|
||||
if (pairingTimestamp == -1L) {
|
||||
state = PairState.NotPaired
|
||||
callback.unpaired()
|
||||
return
|
||||
}
|
||||
val currentTimestamp = System.currentTimeMillis() / 1000L
|
||||
if (abs(pairingTimestamp - currentTimestamp) > allowedTimestampDifferenceSeconds) {
|
||||
state = PairState.NotPaired
|
||||
callback.pairingFailed(device.context.getString(R.string.error_clocks_not_match))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
state = PairState.RequestedByPeer
|
||||
|
||||
pairingScope.launch {
|
||||
@@ -85,6 +108,18 @@ class PairingHandler(private val device: Device, private val callback: PairingCa
|
||||
}
|
||||
}
|
||||
|
||||
fun verificationKey(): String? {
|
||||
return if (device.protocolVersion >= 8) {
|
||||
if (state != PairState.Requested && state != PairState.RequestedByPeer) {
|
||||
return null
|
||||
} else {
|
||||
getVerificationKey(SslHelper.certificate, device.certificate, pairingTimestamp)
|
||||
}
|
||||
} else {
|
||||
getVerificationKeyV7(SslHelper.certificate, device.certificate)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPairing() {
|
||||
cancelTimer()
|
||||
|
||||
@@ -126,6 +161,8 @@ class PairingHandler(private val device: Device, private val callback: PairingCa
|
||||
}
|
||||
val np = NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR)
|
||||
np["pair"] = true
|
||||
pairingTimestamp = System.currentTimeMillis() / 1000L
|
||||
np["timestamp"] = pairingTimestamp
|
||||
device.sendPacket(np, statusCallback)
|
||||
}
|
||||
|
||||
@@ -181,4 +218,36 @@ class PairingHandler(private val device: Device, private val callback: PairingCa
|
||||
private fun cancelTimer() {
|
||||
pairingJob.cancelChildren()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val allowedTimestampDifferenceSeconds = 1_800 // 30 minutes
|
||||
|
||||
// Concatenate in a deterministic order so on both devices the result is the same
|
||||
private fun sortedConcat(a: ByteArray, b: ByteArray): ByteArray {
|
||||
return if (Arrays.compareUnsigned(a, b) < 0) {
|
||||
b + a
|
||||
} else {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
private fun humanReadableHash(bytes: ByteArray): String {
|
||||
val hash = MessageDigest.getInstance("SHA-256").digest(bytes)
|
||||
val formatter = Formatter()
|
||||
for (value in hash) {
|
||||
formatter.format("%02x", value)
|
||||
}
|
||||
return formatter.toString().substring(0, 8).uppercase()
|
||||
}
|
||||
fun getVerificationKey(certificateA: Certificate, certificateB: Certificate, timestamp: Long): String {
|
||||
val certsConcat = sortedConcat(certificateA.publicKey.encoded, certificateB.publicKey.encoded)
|
||||
return humanReadableHash(certsConcat + timestamp.toString().toByteArray())
|
||||
}
|
||||
|
||||
fun getVerificationKeyV7(certificateA: Certificate, certificateB: Certificate): String {
|
||||
val certsConcat = sortedConcat(certificateA.publicKey.encoded, certificateB.publicKey.encoded)
|
||||
return humanReadableHash(certsConcat)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -15,37 +15,46 @@ import android.speech.RecognizerIntent;
|
||||
import android.speech.SpeechRecognizer;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.UserInterface.MainActivity;
|
||||
import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment;
|
||||
import org.kde.kdeconnect.base.BaseActivity;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityBigscreenBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BigscreenActivity extends AppCompatActivity {
|
||||
import kotlin.Lazy;
|
||||
import kotlin.LazyKt;
|
||||
|
||||
public class BigscreenActivity extends BaseActivity<ActivityBigscreenBinding> {
|
||||
|
||||
private static final int REQUEST_SPEECH = 100;
|
||||
|
||||
private final Lazy<ActivityBigscreenBinding> lazyBinding = LazyKt.lazy(() -> ActivityBigscreenBinding.inflate(getLayoutInflater()));
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ActivityBigscreenBinding getBinding() {
|
||||
return lazyBinding.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final ActivityBigscreenBinding binding = ActivityBigscreenBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setSupportActionBar(binding.toolbarLayout.toolbar);
|
||||
setSupportActionBar(getBinding().toolbarLayout.toolbar);
|
||||
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
||||
final String deviceId = getIntent().getStringExtra("deviceId");
|
||||
|
||||
if (!SpeechRecognizer.isRecognitionAvailable(this)) {
|
||||
binding.micButton.setEnabled(false);
|
||||
binding.micButton.setVisibility(View.INVISIBLE);
|
||||
getBinding().micButton.setEnabled(false);
|
||||
getBinding().micButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
BigscreenPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, BigscreenPlugin.class);
|
||||
@@ -54,13 +63,13 @@ public class BigscreenActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.leftButton.setOnClickListener(v -> plugin.sendLeft());
|
||||
binding.rightButton.setOnClickListener(v -> plugin.sendRight());
|
||||
binding.upButton.setOnClickListener(v -> plugin.sendUp());
|
||||
binding.downButton.setOnClickListener(v -> plugin.sendDown());
|
||||
binding.selectButton.setOnClickListener(v -> plugin.sendSelect());
|
||||
binding.homeButton.setOnClickListener(v -> plugin.sendHome());
|
||||
binding.micButton.setOnClickListener(v -> {
|
||||
getBinding().leftButton.setOnClickListener(v -> plugin.sendLeft());
|
||||
getBinding().rightButton.setOnClickListener(v -> plugin.sendRight());
|
||||
getBinding().upButton.setOnClickListener(v -> plugin.sendUp());
|
||||
getBinding().downButton.setOnClickListener(v -> plugin.sendDown());
|
||||
getBinding().selectButton.setOnClickListener(v -> plugin.sendSelect());
|
||||
getBinding().homeButton.setOnClickListener(v -> plugin.sendHome());
|
||||
getBinding().micButton.setOnClickListener(v -> {
|
||||
if (plugin.hasMicPermission()) {
|
||||
activateSTT();
|
||||
} else {
|
||||
|
@@ -25,8 +25,6 @@ import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@PluginFactory.LoadablePlugin
|
||||
public class BigscreenPlugin extends Plugin {
|
||||
|
||||
|
@@ -25,8 +25,6 @@ import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@PluginFactory.LoadablePlugin
|
||||
public class ClipboardPlugin extends Plugin {
|
||||
|
||||
|
@@ -10,26 +10,35 @@ import android.util.Log;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.base.BaseActivity;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityFindMyPhoneBinding;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class FindMyPhoneActivity extends AppCompatActivity {
|
||||
import kotlin.Lazy;
|
||||
import kotlin.LazyKt;
|
||||
|
||||
public class FindMyPhoneActivity extends BaseActivity<ActivityFindMyPhoneBinding> {
|
||||
static final String EXTRA_DEVICE_ID = "deviceId";
|
||||
|
||||
String deviceId;
|
||||
|
||||
private final Lazy<ActivityFindMyPhoneBinding> lazyBinding = LazyKt.lazy(() -> ActivityFindMyPhoneBinding.inflate(getLayoutInflater()));
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ActivityFindMyPhoneBinding getBinding() {
|
||||
return lazyBinding.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final ActivityFindMyPhoneBinding binding = ActivityFindMyPhoneBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setSupportActionBar(binding.toolbarLayout.toolbar);
|
||||
setSupportActionBar(getBinding().toolbarLayout.toolbar);
|
||||
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
||||
@@ -47,7 +56,7 @@ public class FindMyPhoneActivity extends AppCompatActivity {
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
);
|
||||
|
||||
binding.bFindMyPhone.setOnClickListener(view -> finish());
|
||||
getBinding().bFindMyPhone.setOnClickListener(view -> finish());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -37,7 +37,6 @@ import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
@PluginFactory.LoadablePlugin
|
||||
public class FindMyPhonePlugin extends Plugin {
|
||||
|
@@ -30,6 +30,7 @@ import org.kde.kdeconnect.UserInterface.compose.KdeTextButton
|
||||
import org.kde.kdeconnect.UserInterface.compose.KdeTextField
|
||||
import org.kde.kdeconnect.UserInterface.compose.KdeTheme
|
||||
import org.kde.kdeconnect.UserInterface.compose.KdeTopAppBar
|
||||
import org.kde.kdeconnect.extensions.safeDrawPadding
|
||||
import org.kde.kdeconnect_tp.R
|
||||
|
||||
private const val INPUT_CACHE_KEY = "compose_send_input_cache"
|
||||
@@ -91,6 +92,7 @@ class ComposeSendActivity : AppCompatActivity() {
|
||||
private fun ComposeSendScreen() {
|
||||
KdeTheme(this) {
|
||||
Scaffold(
|
||||
modifier = Modifier.safeDrawPadding(),
|
||||
topBar = {
|
||||
KdeTopAppBar(
|
||||
title = stringResource(R.string.compose_send_title),
|
||||
|
@@ -13,7 +13,6 @@ 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;
|
||||
@@ -24,18 +23,23 @@ import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.UserInterface.PluginSettingsActivity;
|
||||
import org.kde.kdeconnect.base.BaseActivity;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityMousepadBinding;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import kotlin.LazyKt;
|
||||
|
||||
public class MousePadActivity
|
||||
extends AppCompatActivity
|
||||
extends BaseActivity<ActivityMousepadBinding>
|
||||
implements GestureDetector.OnGestureListener,
|
||||
GestureDetector.OnDoubleTapListener,
|
||||
MousePadGestureDetector.OnGestureListener,
|
||||
@@ -74,6 +78,14 @@ public class MousePadActivity
|
||||
|
||||
private boolean prefsApplied = false;
|
||||
|
||||
private final Lazy<ActivityMousepadBinding> lazyBinding = LazyKt.lazy(() -> ActivityMousepadBinding.inflate(getLayoutInflater()));
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ActivityMousepadBinding getBinding() {
|
||||
return lazyBinding.getValue();
|
||||
}
|
||||
|
||||
enum ClickType {
|
||||
LEFT, RIGHT, MIDDLE, NONE;
|
||||
|
||||
@@ -131,15 +143,12 @@ public class MousePadActivity
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_mousepad);
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
setSupportActionBar(getBinding().toolbarLayout.toolbar);
|
||||
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
||||
findViewById(R.id.mouse_click_left).setOnClickListener(v -> sendLeftClick());
|
||||
findViewById(R.id.mouse_click_middle).setOnClickListener(v -> sendMiddleClick());
|
||||
findViewById(R.id.mouse_click_right).setOnClickListener(v -> sendRightClick());
|
||||
getBinding().mouseClickLeft.setOnClickListener(v -> sendLeftClick());
|
||||
getBinding().mouseClickMiddle.setOnClickListener(v -> sendMiddleClick());
|
||||
getBinding().mouseClickRight.setOnClickListener(v -> sendRightClick());
|
||||
|
||||
deviceId = getIntent().getStringExtra("deviceId");
|
||||
|
||||
@@ -150,7 +159,7 @@ public class MousePadActivity
|
||||
mDetector.setOnDoubleTapListener(this);
|
||||
mSensorManager = ContextCompat.getSystemService(this, SensorManager.class);
|
||||
|
||||
keyListenerView = findViewById(R.id.keyListener);
|
||||
keyListenerView = getBinding().keyListener;
|
||||
keyListenerView.setDeviceId(deviceId);
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
@@ -582,9 +591,9 @@ public class MousePadActivity
|
||||
}
|
||||
|
||||
if (prefs.getBoolean(getString(R.string.mousepad_mouse_buttons_enabled_pref), true)) {
|
||||
findViewById(R.id.mouse_buttons).setVisibility(View.VISIBLE);
|
||||
getBinding().mouseButtons.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
findViewById(R.id.mouse_buttons).setVisibility(View.GONE);
|
||||
getBinding().mouseButtons.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
doubleTapDragEnabled = prefs.getBoolean(getString(R.string.mousepad_doubletap_drag_enabled_pref), true);
|
||||
|
@@ -19,8 +19,6 @@ import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@PluginFactory.LoadablePlugin
|
||||
public class MousePadPlugin extends Plugin {
|
||||
|
||||
|
@@ -157,9 +157,6 @@ public class PointerAccelerationProfileFactory {
|
||||
|
||||
public static PointerAccelerationProfile getProfileWithName(String name) {
|
||||
switch (name) {
|
||||
case "noacceleration":
|
||||
default:
|
||||
return new DefaultProfile();
|
||||
case "weaker":
|
||||
return new PolynomialProfile(0.25f);
|
||||
case "weak":
|
||||
@@ -170,6 +167,9 @@ public class PointerAccelerationProfileFactory {
|
||||
return new PolynomialProfile(1.5f);
|
||||
case "stronger":
|
||||
return new PolynomialProfile(2.0f);
|
||||
case "noacceleration":
|
||||
default:
|
||||
return new DefaultProfile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,17 +13,18 @@ import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.Helpers.SafeTextChecker;
|
||||
import org.kde.kdeconnect.Helpers.WindowHelper;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.UserInterface.List.EntryItemWithIcon;
|
||||
import org.kde.kdeconnect.UserInterface.List.ListAdapter;
|
||||
import org.kde.kdeconnect.UserInterface.List.SectionItem;
|
||||
import org.kde.kdeconnect.base.BaseActivity;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivitySendkeystrokesBinding;
|
||||
|
||||
@@ -33,7 +34,10 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
import kotlin.Lazy;
|
||||
import kotlin.LazyKt;
|
||||
|
||||
public class SendKeystrokesToHostActivity extends BaseActivity<ActivitySendkeystrokesBinding> {
|
||||
|
||||
// text with these length and content can be send without user confirmation.
|
||||
// more or less chosen arbitrarily, so that we allow short PINS and TANS without interruption (if only one device is connected)
|
||||
@@ -42,19 +46,30 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
public static final String SAFE_CHARS = "1234567890";
|
||||
|
||||
|
||||
private ActivitySendkeystrokesBinding binding;
|
||||
private boolean contentIsOkay;
|
||||
|
||||
private final Lazy<ActivitySendkeystrokesBinding> lazyBinding = LazyKt.lazy(() -> ActivitySendkeystrokesBinding.inflate(getLayoutInflater()));
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ActivitySendkeystrokesBinding getBinding() {
|
||||
return lazyBinding.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrollable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivitySendkeystrokesBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setSupportActionBar(binding.toolbarLayout.toolbar);
|
||||
setSupportActionBar(getBinding().toolbarLayout.toolbar);
|
||||
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
||||
WindowHelper.setupBottomPadding(getBinding().devicesList);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +88,7 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
|
||||
if ("text/x-keystrokes".equals(type)) {
|
||||
String toSend = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
binding.textToSend.setText(toSend);
|
||||
getBinding().textToSend.setText(toSend);
|
||||
|
||||
// if the preference send_safe_text_immediately is true, we will check if exactly one
|
||||
// device is connected and send the text to it without user confirmation, to make sending of
|
||||
@@ -121,7 +136,7 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
|
||||
private void sendKeys(Device deviceId) {
|
||||
String toSend;
|
||||
if (binding.textToSend.getText() != null && (toSend = binding.textToSend.getText().toString().trim()).length() > 0) {
|
||||
if (getBinding().textToSend.getText() != null && (toSend = getBinding().textToSend.getText().toString().trim()).length() > 0) {
|
||||
final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("key", toSend);
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId.getDeviceId(), MousePadPlugin.class);
|
||||
@@ -156,8 +171,8 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
binding.devicesList.setAdapter(new ListAdapter(SendKeystrokesToHostActivity.this, items));
|
||||
binding.devicesList.setOnItemClickListener((adapterView, view, i, l) -> {
|
||||
getBinding().devicesList.setAdapter(new ListAdapter(SendKeystrokesToHostActivity.this, items));
|
||||
getBinding().devicesList.setOnItemClickListener((adapterView, view, i, l) -> {
|
||||
Device device = devicesList.get(i - 1); // NOTE: -1 because of the title!
|
||||
sendKeys(device);
|
||||
this.finish(); // close the activity
|
||||
|
@@ -121,8 +121,12 @@ public class MouseReceiverService extends AccessibilityService {
|
||||
|
||||
new Handler(instance.getMainLooper()).post(() -> {
|
||||
// Log.i("MouseReceiverService", "performing move");
|
||||
instance.windowManager.updateViewLayout(instance.cursorView, instance.cursorLayout);
|
||||
instance.cursorView.setVisibility(View.VISIBLE);
|
||||
try {
|
||||
instance.windowManager.updateViewLayout(instance.cursorView, instance.cursorLayout);
|
||||
instance.cursorView.setVisibility(View.VISIBLE);
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -29,6 +29,7 @@ import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import androidx.core.net.toUri
|
||||
|
||||
/**
|
||||
* Handles the cache for album art
|
||||
@@ -130,7 +131,7 @@ internal object AlbumArtCache {
|
||||
if (albumUrl.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
val url = Uri.parse(albumUrl)
|
||||
val url = albumUrl.toUri()
|
||||
|
||||
//We currently only support http(s), file, and kdeconnect urls
|
||||
if (url.scheme !in ALLOWED_SCHEMES) {
|
||||
@@ -221,7 +222,7 @@ internal object AlbumArtCache {
|
||||
* Does the actual fetching and makes sure only not too many fetches are running at the same time
|
||||
*/
|
||||
private fun initiateFetch() {
|
||||
var url : Uri;
|
||||
var url : Uri
|
||||
synchronized(fetchUrlList) {
|
||||
if (numFetching >= 2 || fetchUrlList.isEmpty()) return
|
||||
//Fetch the last-requested url first, it will probably be needed first
|
||||
@@ -283,7 +284,7 @@ internal object AlbumArtCache {
|
||||
payload.close()
|
||||
return
|
||||
}
|
||||
val url = Uri.parse(albumUrl)
|
||||
val url = albumUrl.toUri()
|
||||
if (url.scheme !in REMOTE_FETCH_SCHEMES) {
|
||||
//Shouldn't happen (checked on receival of the url), but just to be sure
|
||||
Log.e("KDE/Mpris/AlbumArtCache", "Got invalid art url with payload: $albumUrl")
|
||||
|
@@ -8,22 +8,25 @@ package org.kde.kdeconnect.Plugins.MprisPlugin
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.kde.kdeconnect.Plugins.SystemVolumePlugin.SystemVolumeFragment
|
||||
import org.kde.kdeconnect.base.BaseActivity
|
||||
import org.kde.kdeconnect.extensions.viewBinding
|
||||
import org.kde.kdeconnect_tp.R
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityMprisBinding
|
||||
|
||||
class MprisActivity : AppCompatActivity() {
|
||||
private lateinit var activityMprisBinding: ActivityMprisBinding
|
||||
class MprisActivity : BaseActivity<ActivityMprisBinding>() {
|
||||
|
||||
override val binding: ActivityMprisBinding by viewBinding(ActivityMprisBinding::inflate)
|
||||
|
||||
private lateinit var mprisPagerAdapter: MprisPagerAdapter
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
return when (keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
val pagePosition = activityMprisBinding.mprisTabs.selectedTabPosition
|
||||
val pagePosition = binding.mprisTabs.selectedTabPosition
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
mprisPagerAdapter.onVolumeUp(pagePosition)
|
||||
} else {
|
||||
@@ -46,17 +49,13 @@ class MprisActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
activityMprisBinding = ActivityMprisBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(activityMprisBinding.root)
|
||||
|
||||
val deviceId = intent.getStringExtra(MprisPlugin.DEVICE_ID_KEY)
|
||||
|
||||
mprisPagerAdapter = MprisPagerAdapter(this, deviceId)
|
||||
activityMprisBinding.mprisPager.adapter = mprisPagerAdapter
|
||||
binding.mprisPager.adapter = mprisPagerAdapter
|
||||
|
||||
val tabLayoutMediator = TabLayoutMediator(
|
||||
activityMprisBinding.mprisTabs, activityMprisBinding.mprisPager
|
||||
binding.mprisTabs, binding.mprisPager
|
||||
) { tab, position ->
|
||||
tab.setText(
|
||||
mprisPagerAdapter.getTitle(position)
|
||||
@@ -65,7 +64,7 @@ class MprisActivity : AppCompatActivity() {
|
||||
|
||||
tabLayoutMediator.attach()
|
||||
|
||||
setSupportActionBar(activityMprisBinding.toolbar)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
|
@@ -52,7 +52,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
private var spotifyRunning = false
|
||||
|
||||
// Holds the device and player displayed in the notification
|
||||
private var notificationDevice: String? = null
|
||||
private var notificationDeviceId: String? = null
|
||||
private var notificationPlayer: MprisPlayer? = null
|
||||
|
||||
// Holds the device ids for which we can display a notification
|
||||
@@ -64,29 +64,27 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
// Callback for control via the media session API
|
||||
private val mediaSessionCallback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() {
|
||||
override fun onPlay() {
|
||||
notificationPlayer!!.sendPlay()
|
||||
notificationPlayer?.sendPlay()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
notificationPlayer!!.sendPause()
|
||||
notificationPlayer?.sendPause()
|
||||
}
|
||||
|
||||
override fun onSkipToNext() {
|
||||
notificationPlayer!!.sendNext()
|
||||
notificationPlayer?.sendNext()
|
||||
}
|
||||
|
||||
override fun onSkipToPrevious() {
|
||||
notificationPlayer!!.sendPrevious()
|
||||
notificationPlayer?.sendPrevious()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
if (notificationPlayer != null) {
|
||||
notificationPlayer!!.sendStop()
|
||||
}
|
||||
notificationPlayer?.sendStop()
|
||||
}
|
||||
|
||||
override fun onSeekTo(pos: Long) {
|
||||
notificationPlayer!!.sendSetPosition(pos.toInt())
|
||||
notificationPlayer?.sendSetPosition(pos.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +99,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
* @param device The device id
|
||||
*/
|
||||
fun onCreate(context: Context?, plugin: MprisPlugin, device: String) {
|
||||
if (mprisDevices.isEmpty()) {
|
||||
if (mprisDevices.isEmpty) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
@@ -139,7 +137,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
plugin.removePlayerListUpdatedHandler("media_notification")
|
||||
updateMediaNotification()
|
||||
|
||||
if (mprisDevices.isEmpty()) {
|
||||
if (mprisDevices.isEmpty) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
@@ -153,25 +151,27 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
* player and device, while possible.
|
||||
*/
|
||||
private fun updateCurrentPlayer(): MprisPlayer? {
|
||||
val player = findPlayer()
|
||||
val player = findPlayer() ?: return null
|
||||
|
||||
// Update the last-displayed device and player
|
||||
notificationDevice = if (player.first == null) null else player.first!!.deviceId
|
||||
notificationDeviceId = if (player.first == null) null else player.first.deviceId
|
||||
notificationPlayer = player.second
|
||||
return notificationPlayer
|
||||
}
|
||||
|
||||
private fun findPlayer(): Pair<Device?, MprisPlayer?> {
|
||||
private fun findPlayer(): Pair<Device, MprisPlayer>? {
|
||||
val currentDevice = if (notificationDeviceId != null && mprisDevices.contains(notificationDeviceId)) {
|
||||
KdeConnect.getInstance().getDevice(notificationDeviceId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// First try the previously displayed player (if still playing) or the previous displayed device (otherwise)
|
||||
if (notificationDevice != null && mprisDevices.contains(notificationDevice)) {
|
||||
val device = KdeConnect.getInstance().getDevice(notificationDevice)
|
||||
val player = if (notificationPlayer != null && notificationPlayer!!.isPlaying) {
|
||||
getPlayerFromDevice(device, notificationPlayer)
|
||||
} else {
|
||||
getPlayerFromDevice(device, null)
|
||||
}
|
||||
if (currentDevice != null) {
|
||||
val playingPlayer = notificationPlayer?.takeIf { it.isPlaying }
|
||||
val player = getPlayerFromDevice(currentDevice, playingPlayer)
|
||||
if (player != null) {
|
||||
return Pair(device, player)
|
||||
return Pair(currentDevice, player)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,42 +186,33 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
// So no player is playing. Try the previously displayed player again
|
||||
// This will succeed if it's paused:
|
||||
// that allows pausing and subsequently resuming via the notification
|
||||
if (notificationDevice != null && mprisDevices.contains(notificationDevice)) {
|
||||
val device = KdeConnect.getInstance().getDevice(notificationDevice)
|
||||
|
||||
val player = getPlayerFromDevice(device, notificationPlayer)
|
||||
if (currentDevice != null) {
|
||||
val player = getPlayerFromDevice(currentDevice, notificationPlayer)
|
||||
if (player != null) {
|
||||
return Pair(device, player)
|
||||
return Pair(currentDevice, player)
|
||||
}
|
||||
}
|
||||
return Pair(null, null)
|
||||
}
|
||||
|
||||
private fun getPlayerFromDevice(device: Device?, preferredPlayer: MprisPlayer?): MprisPlayer? {
|
||||
if (device == null || !mprisDevices.contains(device.deviceId)) return null
|
||||
|
||||
val plugin = device.getPlugin(MprisPlugin::class.java) ?: return null
|
||||
|
||||
// First try the preferred player, if supplied
|
||||
if (plugin.hasPlayer(preferredPlayer) && shouldShowPlayer(preferredPlayer)) {
|
||||
return preferredPlayer
|
||||
}
|
||||
|
||||
// Otherwise, accept any playing player
|
||||
val player = plugin.playingPlayer
|
||||
if (shouldShowPlayer(player)) {
|
||||
return player
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun shouldShowPlayer(player: MprisPlayer?): Boolean {
|
||||
return player != null && !(player.isSpotify && spotifyRunning)
|
||||
private fun getPlayerFromDevice(device: Device, preferredPlayer: MprisPlayer?): MprisPlayer? {
|
||||
if (!mprisDevices.contains(device.deviceId)) return null
|
||||
|
||||
val plugin = device.getPlugin(MprisPlugin::class.java) ?: return null
|
||||
// First try the preferred player, if supplied & available, otherwise, accept any playing player
|
||||
val player = preferredPlayer?.takeIf(plugin::hasPlayer)
|
||||
?: plugin.playingPlayer
|
||||
?: return null
|
||||
return player.takeIf(::shouldShowPlayer)
|
||||
}
|
||||
|
||||
private fun shouldShowPlayer(player: MprisPlayer): Boolean {
|
||||
return !(player.isSpotify && spotifyRunning)
|
||||
}
|
||||
|
||||
private fun updateRemoteDeviceVolumeControl() {
|
||||
val plugin = KdeConnect.getInstance().getDevicePlugin(notificationDevice, SystemVolumePlugin::class.java)
|
||||
val plugin = KdeConnect.getInstance().getDevicePlugin(notificationDeviceId, SystemVolumePlugin::class.java)
|
||||
?: return
|
||||
val systemVolumeProvider = fromPlugin(plugin)
|
||||
systemVolumeProvider.addStateListener(this)
|
||||
@@ -250,7 +241,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
// Make sure our information is up-to-date
|
||||
val currentPlayer = updateCurrentPlayer()
|
||||
|
||||
val device = KdeConnect.getInstance().getDevice(notificationDevice)
|
||||
val device = KdeConnect.getInstance().getDevice(notificationDeviceId)
|
||||
if (device == null) {
|
||||
closeMediaNotification()
|
||||
return
|
||||
@@ -295,7 +286,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
// Create all actions (previous/play/pause/next)
|
||||
val iPlay = Intent(context, MprisMediaNotificationReceiver::class.java).apply {
|
||||
setAction(MprisMediaNotificationReceiver.ACTION_PLAY)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDeviceId)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.playerName)
|
||||
}
|
||||
val piPlay = PendingIntent.getBroadcast(
|
||||
@@ -310,7 +301,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
|
||||
val iPause = Intent(context, MprisMediaNotificationReceiver::class.java).apply {
|
||||
setAction(MprisMediaNotificationReceiver.ACTION_PAUSE)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDeviceId)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.playerName)
|
||||
}
|
||||
val piPause = PendingIntent.getBroadcast(
|
||||
@@ -325,7 +316,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
|
||||
val iPrevious = Intent(context, MprisMediaNotificationReceiver::class.java).apply {
|
||||
setAction(MprisMediaNotificationReceiver.ACTION_PREVIOUS)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDeviceId)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.playerName)
|
||||
}
|
||||
val piPrevious = PendingIntent.getBroadcast(
|
||||
@@ -340,7 +331,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
|
||||
val iNext = Intent(context, MprisMediaNotificationReceiver::class.java).apply {
|
||||
setAction(MprisMediaNotificationReceiver.ACTION_NEXT)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDeviceId)
|
||||
putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.playerName)
|
||||
}
|
||||
val piNext = PendingIntent.getBroadcast(
|
||||
@@ -354,7 +345,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
)
|
||||
|
||||
val iOpenActivity = Intent(context, MprisActivity::class.java).apply {
|
||||
putExtra("deviceId", notificationDevice)
|
||||
putExtra("deviceId", notificationDeviceId)
|
||||
putExtra("player", currentPlayer.playerName)
|
||||
}
|
||||
|
||||
@@ -392,7 +383,7 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
if (!currentPlayer.isPlaying) {
|
||||
val iCloseNotification = Intent(context, MprisMediaNotificationReceiver::class.java)
|
||||
iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION)
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice)
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDeviceId)
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, currentPlayer.playerName)
|
||||
val piCloseNotification = PendingIntent.getBroadcast(
|
||||
context,
|
||||
@@ -450,17 +441,18 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
|
||||
// Display the notification
|
||||
synchronized(instance) {
|
||||
if (mediaSession == null) {
|
||||
mediaSession = MediaSessionCompat(context!!, MPRIS_MEDIA_SESSION_TAG)
|
||||
mediaSession!!.setCallback(mediaSessionCallback, Handler(context!!.mainLooper))
|
||||
val mediaSession = mediaSession ?: MediaSessionCompat(context!!, MPRIS_MEDIA_SESSION_TAG).apply {
|
||||
setCallback(mediaSessionCallback, Handler(context!!.mainLooper))
|
||||
}
|
||||
mediaSession!!.setMetadata(metadata.build())
|
||||
mediaSession!!.setPlaybackState(playbackState.build())
|
||||
mediaStyle.setMediaSession(mediaSession!!.sessionToken)
|
||||
mediaSession.setMetadata(metadata.build())
|
||||
mediaSession.setPlaybackState(playbackState.build())
|
||||
mediaStyle.setMediaSession(mediaSession.sessionToken)
|
||||
notification.setStyle(mediaStyle)
|
||||
mediaSession!!.isActive = true
|
||||
val nm = ContextCompat.getSystemService(context!!, NotificationManager::class.java)
|
||||
nm!!.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build())
|
||||
mediaSession.isActive = true
|
||||
ContextCompat.getSystemService(context!!, NotificationManager::class.java)?.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build())
|
||||
if (this.mediaSession == null) {
|
||||
this.mediaSession = mediaSession
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,16 +464,14 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
// Clear the current player and media session
|
||||
notificationPlayer = null
|
||||
synchronized(instance) {
|
||||
if (mediaSession != null) {
|
||||
mediaSession!!.setPlaybackState(PlaybackStateCompat.Builder().build())
|
||||
mediaSession!!.setMetadata(MediaMetadataCompat.Builder().build())
|
||||
mediaSession!!.isActive = false
|
||||
mediaSession!!.release()
|
||||
mediaSession = null
|
||||
|
||||
val currentProvider = currentProvider
|
||||
currentProvider?.release()
|
||||
mediaSession?.apply {
|
||||
setPlaybackState(PlaybackStateCompat.Builder().build())
|
||||
setMetadata(MediaMetadataCompat.Builder().build())
|
||||
isActive = false
|
||||
release()
|
||||
}
|
||||
mediaSession = null
|
||||
currentProvider?.release()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,8 +519,8 @@ class MprisMediaSession : OnSharedPreferenceChangeListener, NotificationReceiver
|
||||
}
|
||||
}
|
||||
|
||||
private fun StatusBarNotification?.isSpotify(): Boolean =
|
||||
this?.packageName == SPOTIFY_PACKAGE_NAME
|
||||
private fun StatusBarNotification.isSpotify(): Boolean =
|
||||
this.packageName == SPOTIFY_PACKAGE_NAME
|
||||
|
||||
companion object {
|
||||
const val TAG = "MprisMediaSession"
|
||||
|
@@ -34,6 +34,7 @@ import org.kde.kdeconnect_tp.databinding.MprisNowPlayingBinding
|
||||
import java.net.MalformedURLException
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import androidx.core.net.toUri
|
||||
|
||||
private typealias MprisPlayerCallback = (MprisPlayer) -> Unit
|
||||
|
||||
@@ -55,24 +56,23 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
|
||||
val activityIntent = requireActivity().intent
|
||||
|
||||
targetPlayerName = if (activityIntent.hasExtra("player")) {
|
||||
activityIntent.getStringExtra("player")!!.also {
|
||||
activityIntent.removeExtra("player")
|
||||
}
|
||||
} else {
|
||||
savedInstanceState?.getString("targetPlayer") ?: "".also {
|
||||
val stringExtra = activityIntent.getStringExtra("player")
|
||||
if (stringExtra != null) {
|
||||
activityIntent.removeExtra("player")
|
||||
}
|
||||
targetPlayerName = stringExtra
|
||||
?: savedInstanceState?.getString("targetPlayer")
|
||||
?: "".also {
|
||||
Log.i("MprisNowPlayingFragment", "No `targetPlayer` specified in savedInstanceState")
|
||||
}
|
||||
}
|
||||
|
||||
connectToPlugin()
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val interval_time_str = prefs.getString(
|
||||
val intervalTime = prefs.getString(
|
||||
getString(R.string.mpris_time_key),
|
||||
getString(R.string.mpris_time_default)
|
||||
)
|
||||
val interval_time = interval_time_str!!.toInt()
|
||||
)!!.toInt()
|
||||
|
||||
performActionOnClick(mprisControlBinding.loopButton) { p: MprisPlayer ->
|
||||
when (p.loopStatus) {
|
||||
@@ -92,11 +92,11 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
|
||||
performActionOnClick(
|
||||
mprisControlBinding.rewButton
|
||||
) { p -> p.sendSeek(interval_time * -1) }
|
||||
) { p -> p.sendSeek(intervalTime * -1) }
|
||||
|
||||
performActionOnClick(
|
||||
mprisControlBinding.ffButton
|
||||
) { p -> p.sendSeek(interval_time) }
|
||||
) { p -> p.sendSeek(intervalTime) }
|
||||
|
||||
performActionOnClick(mprisControlBinding.nextButton, MprisPlayer::sendNext)
|
||||
|
||||
@@ -116,8 +116,8 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
positionSeekUpdateRunnable = Runnable {
|
||||
if (!isAdded) return@Runnable // Fragment was already detached
|
||||
|
||||
if (targetPlayer != null) {
|
||||
mprisControlBinding.positionSeek.progress = targetPlayer!!.position.toInt()
|
||||
targetPlayer?.let {
|
||||
mprisControlBinding.positionSeek.progress = it.position.toInt()
|
||||
}
|
||||
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable)
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000)
|
||||
@@ -134,9 +134,7 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
if (targetPlayer != null) {
|
||||
targetPlayer!!.sendSetPosition(seekBar.progress)
|
||||
}
|
||||
targetPlayer?.sendSetPosition(seekBar.progress)
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200)
|
||||
}
|
||||
})
|
||||
@@ -199,20 +197,20 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
mprisControlBinding.playerSpinner.onItemSelectedListener =
|
||||
object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(arg0: AdapterView<*>?, arg1: View?, pos: Int, id: Long) {
|
||||
|
||||
if (pos >= playerList.size) return
|
||||
|
||||
val player = playerList[pos]
|
||||
if (targetPlayer != null && player == targetPlayer!!.playerName) {
|
||||
if (player == targetPlayer?.playerName) {
|
||||
return //Player hasn't actually changed
|
||||
}
|
||||
targetPlayer = plugin.getPlayerStatus(player)
|
||||
if (targetPlayer != null) {
|
||||
targetPlayerName = targetPlayer!!.playerName
|
||||
targetPlayer = plugin.getPlayerStatus(player)?.also {
|
||||
targetPlayerName = it.playerName
|
||||
}
|
||||
|
||||
updatePlayerStatus(plugin)
|
||||
|
||||
if (targetPlayer != null && targetPlayer!!.isPlaying) {
|
||||
if (targetPlayer?.isPlaying == true) {
|
||||
MprisMediaSession.instance.playerSelected(targetPlayer)
|
||||
}
|
||||
}
|
||||
@@ -227,8 +225,8 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
targetPlayer = plugin.playingPlayer
|
||||
}
|
||||
//Try to select the specified player
|
||||
if (targetPlayer != null) {
|
||||
val targetIndex = adapter.getPosition(targetPlayer!!.playerName)
|
||||
targetPlayer?.let {
|
||||
val targetIndex = adapter.getPosition(it.playerName)
|
||||
if (targetIndex >= 0) {
|
||||
mprisControlBinding.playerSpinner.setSelection(targetIndex)
|
||||
} else {
|
||||
@@ -275,8 +273,7 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
val albumArt = playerStatus.getAlbumArt()
|
||||
if (albumArt == null) {
|
||||
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_album_art_placeholder)!!
|
||||
val placeholder_art = DrawableCompat.wrap(drawable)
|
||||
activityMprisBinding.albumArt.setImageDrawable(placeholder_art)
|
||||
activityMprisBinding.albumArt.setImageDrawable(DrawableCompat.wrap(drawable))
|
||||
} else {
|
||||
activityMprisBinding.albumArt.setImageBitmap(albumArt)
|
||||
}
|
||||
@@ -345,12 +342,12 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
* @param step step size volume change
|
||||
*/
|
||||
private fun updateVolume(step: Int) {
|
||||
if (targetPlayer == null) return
|
||||
val targetPlayer = targetPlayer ?: return
|
||||
|
||||
val newVolume = calculateNewVolume(targetPlayer!!.volume, DEFAULT_MAX_VOLUME, step)
|
||||
val newVolume = calculateNewVolume(targetPlayer.volume, DEFAULT_MAX_VOLUME, step)
|
||||
|
||||
if (targetPlayer!!.volume != newVolume) {
|
||||
targetPlayer!!.sendSetVolume(newVolume)
|
||||
if (targetPlayer.volume != newVolume) {
|
||||
targetPlayer.sendSetVolume(newVolume)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,18 +361,19 @@ class MprisNowPlayingFragment : Fragment(), VolumeKeyListener {
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
menu.clear()
|
||||
if (targetPlayer != null && "" != targetPlayer!!.url) {
|
||||
if (!targetPlayer?.url.isNullOrEmpty()) {
|
||||
menu.add(0, MENU_OPEN_URL, Menu.NONE, R.string.mpris_open_url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val targetPlayer = targetPlayer
|
||||
if (targetPlayer != null && item.itemId == MENU_OPEN_URL) {
|
||||
try {
|
||||
val url = VideoUrlsHelper.formatUriWithSeek(targetPlayer!!.url, targetPlayer!!.position).toString()
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val url = VideoUrlsHelper.formatUriWithSeek(targetPlayer.url, targetPlayer.position).toString()
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
targetPlayer!!.sendPause()
|
||||
targetPlayer.sendPause()
|
||||
return true
|
||||
} catch (e: MalformedURLException) {
|
||||
e.printStackTrace()
|
||||
|
@@ -33,6 +33,7 @@ import org.kde.kdeconnect.UserInterface.PluginSettingsFragment
|
||||
import org.kde.kdeconnect_tp.R
|
||||
import java.net.MalformedURLException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@LoadablePlugin
|
||||
class MprisPlugin : Plugin() {
|
||||
@@ -176,14 +177,10 @@ class MprisPlugin : Plugin() {
|
||||
}
|
||||
}
|
||||
|
||||
fun interface Callback {
|
||||
fun callback()
|
||||
}
|
||||
|
||||
private val players = ConcurrentHashMap<String, MprisPlayer>()
|
||||
private var supportAlbumArtPayload = false
|
||||
private val playerStatusUpdated = ConcurrentHashMap<String, Callback>()
|
||||
private val playerListUpdated = ConcurrentHashMap<String, Callback>()
|
||||
private val playerStatusUpdated = ConcurrentHashMap<String, () -> Unit>()
|
||||
private val playerListUpdated = ConcurrentHashMap<String, () -> Unit>()
|
||||
override val displayName: String
|
||||
get() = context.resources.getString(R.string.pref_plugin_mpris)
|
||||
|
||||
@@ -277,7 +274,7 @@ class MprisPlugin : Plugin() {
|
||||
playerStatus.isGoPreviousAllowed = np.getBoolean("canGoPrevious", playerStatus.isGoPreviousAllowed)
|
||||
playerStatus.seekAllowed = np.getBoolean("canSeek", playerStatus.seekAllowed)
|
||||
val newAlbumArtUrlString = np.getString("albumArtUrl", playerStatus.albumArtUrl)
|
||||
val newAlbumArtUrl = Uri.parse(newAlbumArtUrlString)
|
||||
val newAlbumArtUrl = newAlbumArtUrlString.toUri()
|
||||
if (newAlbumArtUrl.scheme in AlbumArtCache.ALLOWED_SCHEMES) {
|
||||
playerStatus.albumArtUrl = newAlbumArtUrl.toString()
|
||||
} else {
|
||||
@@ -285,14 +282,7 @@ class MprisPlugin : Plugin() {
|
||||
playerStatus.albumArtUrl = ""
|
||||
}
|
||||
|
||||
for (key in playerStatusUpdated.keys) {
|
||||
try {
|
||||
playerStatusUpdated[key]!!.callback()
|
||||
} catch (e: Exception) {
|
||||
Log.e("MprisControl", "Exception", e)
|
||||
playerStatusUpdated.remove(key)
|
||||
}
|
||||
}
|
||||
notifyPlayerStatusUpdated()
|
||||
|
||||
// Check to see if a stream has stopped playing and we should deliver a notification
|
||||
if (np.has("isPlaying") && !playerStatus.isPlaying && wasPlaying) {
|
||||
@@ -331,14 +321,7 @@ class MprisPlugin : Plugin() {
|
||||
}
|
||||
}
|
||||
if (!equals) {
|
||||
playerListUpdated.forEach { (key, callback) ->
|
||||
runCatching {
|
||||
callback.callback()
|
||||
}.onFailure {
|
||||
Log.e("MprisControl", "Exception", it)
|
||||
playerListUpdated.remove(key)
|
||||
}
|
||||
}
|
||||
notifyPlayerListUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +335,7 @@ class MprisPlugin : Plugin() {
|
||||
) {
|
||||
try {
|
||||
val url = VideoUrlsHelper.formatUriWithSeek(playerStatus.url, playerStatus.position).toString()
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
val pendingIntent = PendingIntent.getActivity(context, 0, browserIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val notificationManager = ContextCompat.getSystemService(context, NotificationManager::class.java)
|
||||
@@ -368,7 +351,7 @@ class MprisPlugin : Plugin() {
|
||||
builder.build()
|
||||
)
|
||||
} catch (e: MalformedURLException) {
|
||||
e.printStackTrace();
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,25 +360,46 @@ class MprisPlugin : Plugin() {
|
||||
|
||||
override val outgoingPacketTypes: Array<String> = arrayOf(PACKET_TYPE_MPRIS_REQUEST)
|
||||
|
||||
fun setPlayerStatusUpdatedHandler(id: String, h: Callback) {
|
||||
playerStatusUpdated[id] = h
|
||||
h.callback()
|
||||
fun setPlayerStatusUpdatedHandler(id: String, callback: () -> Unit) {
|
||||
playerStatusUpdated[id] = callback
|
||||
callback()
|
||||
}
|
||||
|
||||
fun removePlayerStatusUpdatedHandler(id: String) {
|
||||
playerStatusUpdated.remove(id)
|
||||
}
|
||||
|
||||
fun setPlayerListUpdatedHandler(id: String, h: Callback) {
|
||||
playerListUpdated[id] = h
|
||||
fun notifyPlayerStatusUpdated() {
|
||||
for ((key, callback) in playerStatusUpdated) {
|
||||
try {
|
||||
callback()
|
||||
} catch(e: Exception) {
|
||||
Log.e("MprisControl", "Exception", e)
|
||||
playerStatusUpdated.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.callback()
|
||||
fun setPlayerListUpdatedHandler(id: String, callback: () -> Unit) {
|
||||
playerListUpdated[id] = callback
|
||||
callback()
|
||||
}
|
||||
|
||||
fun removePlayerListUpdatedHandler(id: String) {
|
||||
playerListUpdated.remove(id)
|
||||
}
|
||||
|
||||
fun notifyPlayerListUpdated() {
|
||||
for ((key, callback) in playerListUpdated) {
|
||||
try {
|
||||
callback()
|
||||
} catch(e: Exception) {
|
||||
Log.e("MprisControl", "Exception", e)
|
||||
playerListUpdated.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val playerList: List<String>
|
||||
get() = players.keys.sorted()
|
||||
|
||||
@@ -413,7 +417,7 @@ class MprisPlugin : Plugin() {
|
||||
*/
|
||||
get() = players.values.stream().filter(MprisPlayer::isPlaying).findFirst().orElse(null)
|
||||
|
||||
fun hasPlayer(player: MprisPlayer?): Boolean = player != null && players.containsValue(player)
|
||||
fun hasPlayer(player: MprisPlayer): Boolean = players.containsValue(player)
|
||||
|
||||
private fun requestPlayerList() {
|
||||
val np = NetworkPacket(PACKET_TYPE_MPRIS_REQUEST).apply {
|
||||
@@ -445,14 +449,7 @@ class MprisPlugin : Plugin() {
|
||||
|
||||
fun fetchedAlbumArt(url: String) {
|
||||
if (players.values.stream().anyMatch { player -> url == player.albumArtUrl }) {
|
||||
playerStatusUpdated.forEach { (key, callback) ->
|
||||
runCatching {
|
||||
callback.callback()
|
||||
}.onFailure {
|
||||
Log.e("MprisControl", "Exception", it)
|
||||
playerStatusUpdated.remove(key)
|
||||
}
|
||||
}
|
||||
notifyPlayerStatusUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,14 +3,9 @@
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin
|
||||
|
||||
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin;
|
||||
|
||||
public interface VolumeKeyListener {
|
||||
|
||||
void onVolumeUp();
|
||||
|
||||
void onVolumeDown();
|
||||
|
||||
interface VolumeKeyListener {
|
||||
fun onVolumeUp()
|
||||
fun onVolumeDown()
|
||||
}
|
@@ -280,8 +280,10 @@ public class MprisReceiverPlugin extends Plugin {
|
||||
np.set("canSeek", player.canSeek());
|
||||
np.set("volume", player.getVolume());
|
||||
MprisReceiverCallback cb = playerCbs.get(player.getName());
|
||||
assert cb != null;
|
||||
String artUrl = cb.getArtUrl();
|
||||
String artUrl = null;
|
||||
if (cb != null) {
|
||||
artUrl = cb.getArtUrl();
|
||||
}
|
||||
if (artUrl != null) {
|
||||
np.set("albumArtUrl", artUrl);
|
||||
Log.v(TAG, "Sending metadata with url " + artUrl);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user