Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ad725e7a24 | ||
|
947cd54cb0 | ||
|
14270ea71a | ||
|
46ecd2b05f | ||
|
2839793e9d | ||
|
ad53138928 | ||
|
3a05548333 | ||
|
d0ec7dd755 | ||
|
53f14e9c77 | ||
|
853e5b1903 | ||
|
6b450d558e | ||
|
b065d5c1d1 | ||
|
c70d03dbe8 | ||
|
9e39cbd979 | ||
|
1e904fcbfa | ||
|
38edbd260a | ||
|
62cf2d6b44 | ||
|
f172ffd129 | ||
|
0f3ad63ee3 | ||
|
ea1675c76a | ||
|
40b05fbe8f | ||
|
ae23413971 | ||
|
a6eea8e996 | ||
|
58a304a81e | ||
|
da3ee10567 | ||
|
f6ea3e01b0 | ||
|
e372c47789 | ||
|
72af26016b | ||
|
e17073bb7b | ||
|
faedc2e6c8 | ||
|
4081dc593d | ||
|
2246d3f458 | ||
|
9f64cbbfaf | ||
|
d2d43c55a7 | ||
|
948d075fb3 | ||
|
e409ec37eb | ||
|
ed0f97de1b | ||
|
961a839ac3 | ||
|
b189556d6a | ||
|
ddd2e741f4 | ||
|
4f1cff22ab | ||
|
72130c0cfa | ||
|
2147dc5313 | ||
|
52a27790d0 | ||
|
9c18f3f799 | ||
|
7e32d2962f | ||
|
5d2b9557e7 | ||
|
891da46c3c | ||
|
e9bc90d91a | ||
|
921d0ee884 | ||
|
3e8948339d | ||
|
e56f73da83 | ||
|
c250d2c674 | ||
|
c15469f477 | ||
|
73f15149b6 | ||
|
118a35c304 | ||
|
acb869b21c | ||
|
71706879d0 | ||
|
f5b3523ec6 | ||
|
8639938584 | ||
|
011ee20fbb | ||
|
763859d478 | ||
|
c19019a500 | ||
|
b1a2257d4d | ||
|
32d6a346ab | ||
|
6adb73bf5e |
@@ -7,7 +7,7 @@
|
||||
# - Set up gitlab-runner, as described here: https://stackoverflow.com/a/52724374
|
||||
# - Run `gitlab-runner exec docker --docker-privileged assembleDebug`
|
||||
|
||||
image: openjdk:11-jdk
|
||||
image: eclipse-temurin:17-jdk-focal
|
||||
|
||||
variables:
|
||||
ANDROID_COMPILE_SDK: "31"
|
||||
|
@@ -2,15 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.kde.kdeconnect_tp"
|
||||
android:versionCode="12402"
|
||||
android:versionName="1.24.2">
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="true"
|
||||
android:xlargeScreens="true" />
|
||||
android:versionCode="12500"
|
||||
android:versionName="1.25.0">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
@@ -59,7 +52,7 @@
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:theme="@style/KdeConnectTheme.NoActionBar"
|
||||
android:name="org.kde.kdeconnect.MyApplication"
|
||||
android:name="org.kde.kdeconnect.KdeConnect"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
|
||||
<receiver
|
||||
@@ -275,9 +268,10 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.kde.kdeconnect.Plugins.MousePadPlugin.ComposeSendActivity"
|
||||
android:label="Compose send"
|
||||
android:label="@string/compose_send_title"
|
||||
android:exported="false"
|
||||
android:parentActivityName="org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadActivity">
|
||||
android:parentActivityName="org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadActivity" />
|
||||
@@ -365,6 +359,7 @@
|
||||
<action android:name="android.service.chooser.ChooserTargetService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<!--
|
||||
<service
|
||||
android:name="org.kde.kdeconnect.Plugins.MouseReceiverPlugin.MouseReceiverService"
|
||||
android:exported="true"
|
||||
@@ -376,6 +371,7 @@
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/mouse_receiver_service" />
|
||||
</service>
|
||||
-->
|
||||
|
||||
<activity
|
||||
android:name="org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationFilterActivity"
|
||||
|
33
build.gradle
@@ -4,9 +4,9 @@ import com.android.build.gradle.api.ApplicationVariant
|
||||
import com.github.jk1.license.render.TextReportRenderer
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.8.10'
|
||||
ext.kotlin_version = '1.8.21'
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath 'com.android.tools.build:gradle:8.0.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'org.kde.kdeconnect_tp'
|
||||
compileSdkVersion 33
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
@@ -28,7 +29,13 @@ android {
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.7"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
@@ -81,6 +88,18 @@ android {
|
||||
abortOnError false
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
jvmArgs += [
|
||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/java.security=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/sun.security.rsa=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/sun.security.x509=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,9 +157,15 @@ ext {
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
|
||||
implementation 'androidx.compose.material3:material3:1.1.0'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview:1.4.3'
|
||||
implementation 'androidx.activity:activity-compose:1.7.1'
|
||||
implementation 'com.google.accompanist:accompanist-themeadapter-material3:0.31.0-alpha'
|
||||
implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.1'
|
||||
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.10.0'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
@@ -150,7 +175,7 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.1'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2' //For caching album art bitmaps
|
||||
implementation 'com.jaredrummler:android-device-names:1.1.9' //To get a human-friendly device name
|
||||
|
||||
|
2
fastlane/Appfile
Normal file
@@ -0,0 +1,2 @@
|
||||
json_key_file("/please/pass/the/key/using/--json-key/instead")
|
||||
package_name("org.kde.kdeconnect_tp")
|
3
fastlane/metadata/android/en-US/changelogs/12500.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
1.25.0:
|
||||
* Rewrite some of the internals to improve performance.
|
||||
* Add search by name in the notification plugin settings.
|
14
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
KDE Connect provides a set of features to integrate your workflow across devices:
|
||||
|
||||
- Shared clipboard: copy and paste between your devices.
|
||||
- Share files and URLs to your computer from any app.
|
||||
- Get notifications for incoming calls and SMS messages on your PC.
|
||||
- Virtual touchpad: Use your phone screen as your computer's touchpad.
|
||||
- Notifications sync: Read your Android notifications from the desktop.
|
||||
- Multimedia remote control: Use your phone as a remote for Linux media players.
|
||||
- WiFi connection: no USB wire or bluetooth needed.
|
||||
- End-to-end TLS encryption: your information is safe.
|
||||
|
||||
Please note you will need to install KDE Connect on your computer for this app to work, and keep the desktop version up-to-date with the Android version for the latest features to work.
|
||||
|
||||
This app is part of an open source project and it exists thanks to all the people who contributed to it. Visit the website to grab the source code.
|
BIN
fastlane/metadata/android/en-US/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 698 KiB |
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 216 KiB |
After Width: | Height: | Size: 158 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect integrates your smartphone and computer
|
1
fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
KDE Connect
|
@@ -1,3 +1,4 @@
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.enableJetifier=false
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx4096m
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
|
||||
|
@@ -18,7 +18,7 @@ msgstr ""
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: dummy:1
|
||||
msgid "Integrate Android with the KDE Plasma Desktop."
|
||||
|
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: kdeorg\n"
|
||||
"Report-Msgid-Bugs-To: https://bugs.kde.org\n"
|
||||
"POT-Creation-Date: 2019-06-30 11:38+0200\n"
|
||||
"PO-Revision-Date: 2023-04-16 12:31\n"
|
||||
"PO-Revision-Date: 2023-05-22 14:00\n"
|
||||
"Last-Translator: Albert Vaca Cintora <albertvaka@gmail.com>\n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
|
5
res/drawable/ic_search_24.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
||||
</vector>
|
@@ -16,7 +16,8 @@
|
||||
android:id="@+id/coordinatorLayout"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
tools:context="org.kde.kdeconnect.UserInterface.MainActivity">
|
||||
tools:context="org.kde.kdeconnect.UserInterface.MainActivity"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout" />
|
||||
|
||||
|
@@ -5,7 +5,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="org.kde.kdeconnect.UserInterface.About.AboutKDEActivity">
|
||||
tools:context="org.kde.kdeconnect.UserInterface.About.AboutKDEActivity"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout" />
|
||||
|
||||
|
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/compose"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:ems="10"
|
||||
android:hint="@string/click_here_to_type"
|
||||
android:imeActionLabel="@string/send_compose"
|
||||
android:imeOptions="actionSend|actionDone"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textLongMessage|textMultiLine"
|
||||
android:isScrollContainer="true"
|
||||
android:saveEnabled="true"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appBarLayout2"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
tools:ignore="SpeakableTextPresentCheck,TextContrastCheck" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@@ -6,7 +6,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="org.kde.kdeconnect.UserInterface.CustomDevicesActivity">
|
||||
tools:context="org.kde.kdeconnect.UserInterface.CustomDevicesActivity"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout" />
|
||||
|
||||
|
@@ -4,6 +4,7 @@
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="org.kde.kdeconnect.UserInterface.About.LicensesActivity">
|
||||
|
||||
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout" />
|
||||
|
@@ -7,14 +7,14 @@
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"> <!-- fitSystemWindows to make the drawer slide below the Lollipop transparent status bar -->
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/coordinatorLayout"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
tools:context="org.kde.kdeconnect.UserInterface.MainActivity">
|
||||
tools:context="org.kde.kdeconnect.UserInterface.MainActivity"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout"/>
|
||||
|
||||
|
@@ -9,9 +9,7 @@
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="8dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
@@ -21,10 +19,7 @@
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/mpris_tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/toolbar_color"
|
||||
app:tabIndicatorColor="?android:textColorPrimary"
|
||||
app:tabSelectedTextColor="?android:textColorPrimary" />
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
@@ -6,7 +6,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="org.kde.kdeconnect.UserInterface.PluginSettingsActivity">
|
||||
tools:context="org.kde.kdeconnect.UserInterface.PluginSettingsActivity"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<include layout="@layout/toolbar" android:id="@+id/toolbar_layout" />
|
||||
|
||||
|
@@ -5,12 +5,11 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:elevation="8dp"
|
||||
app:title="@string/kde_connect"/>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:kdeconnect="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_send_compose"
|
||||
android:icon="@android:drawable/ic_menu_send"
|
||||
android:title="@string/send_compose"
|
||||
kdeconnect:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/menu_clear_compose"
|
||||
android:title="@string/clear_compose"
|
||||
kdeconnect:showAsAction="always" />
|
||||
|
||||
</menu>
|
@@ -6,13 +6,13 @@
|
||||
android:id="@+id/menu_rise_up"
|
||||
android:icon="@drawable/ic_arrow_upward_black_24dp"
|
||||
android:title="@string/rise_up"
|
||||
android:iconTint="@color/text_color"
|
||||
kdeconnect:iconTint="?colorOnSurfaceVariant"
|
||||
kdeconnect:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_rise_down"
|
||||
android:icon="@drawable/ic_arrow_downward_black_24dp"
|
||||
android:title="@string/rise_down"
|
||||
android:iconTint="@color/text_color"
|
||||
kdeconnect:iconTint="?colorOnSurfaceVariant"
|
||||
kdeconnect:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
|
@@ -51,11 +51,13 @@
|
||||
<string name="remotekeyboard_connected">Uzaq klaviatura bağlantısını aktiv edin</string>
|
||||
<string name="remotekeyboard_multiple_connections">Birdən çox uzaq klaviatura bağlantısı var, tənzimləmək üçün cihazı seçin</string>
|
||||
<string name="open_mousepad">Məsafədən giriş</string>
|
||||
<string name="mousepad_info">Siçan kursorunu hərəkət etdirmək üçün barmağı ekranda sürüşdürün. Klik üçün ekrana vurun, sağ və orta siçan düymələri üçün iki/üç barmaqla toxunuş edin. Sürüşdürmək üçün iki barmaqdan istifadə edin. Hiroskop siçan funksionallığı plaqin ayarlarında aktiv edilməlidir</string>
|
||||
<string name="mousepad_keyboard_input_not_supported">Qoşulmuş cihaz üçün klaviatura ilə daxiletmə dəstəklənmir</string>
|
||||
<string name="mousepad_single_tap_settings_title">Bir barmaq toxunuşu əməlini təyin edin</string>
|
||||
<string name="mousepad_double_tap_settings_title">İki barmaq toxunuşu əməlini təyin edin</string>
|
||||
<string name="mousepad_triple_tap_settings_title">Üç barmaq toxunuşu əməlini təyin edin</string>
|
||||
<string name="mousepad_sensitivity_settings_title">Toxunma panelinin həsassləğını təyin edin</string>
|
||||
<string name="mousepad_mouse_buttons_title">Siçan düymələrini göstərmək</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Kursorun sürətini təyin edin</string>
|
||||
<string name="mousepad_scroll_direction_title">Sürüşdürmənin əks istiqaməti</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
@@ -216,8 +218,11 @@
|
||||
<string name="sftp_action_mode_menu_delete">Silmək</string>
|
||||
<string name="sftp_no_storage_locations_configured">Saxlama yeri tənzimlənməyib</string>
|
||||
<string name="sftp_saf_permission_explanation">Fayllara uzaqdan daxil olmaq üçün saxlama yerlərini konfiqurasiya etməlisiniz</string>
|
||||
<string name="sftp_manage_storage_permission_explanation">Bu cihazdakı fayllara giriş əldə etmək üçün KDE Connect-ə yaddaşı idarə etməyə icazə vermək lazımdır.</string>
|
||||
<string name="no_players_connected">Pleyer tapılmadı</string>
|
||||
<string name="send_files">Faylları göndərmək</string>
|
||||
<string name="block_notification_contents">Bildirilərin tərkiblərini kilidləmək</string>
|
||||
<string name="block_notification_images">Bildiriş şəlkillərini kilidləmək</string>
|
||||
<string name="pairing_title">KDE Connect Cihazları</string>
|
||||
<string name="pairing_description">Eyni şəbəkədəki KDE Connect işləyən digər cihazlar burada görünməlidir</string>
|
||||
<string name="device_rename_title">Cihazın adını dəyişmək</string>
|
||||
@@ -240,8 +245,10 @@
|
||||
<string name="close">Bağlamaq</string>
|
||||
<string name="plugins_need_permission">Bəzi qoşmaların işləməsi üçün icazələr lazımdır (daha çox məlumat üçün toxunun):</string>
|
||||
<string name="permission_explanation">Bu qoşmanın işləməsi üçün icazələr lazımdır</string>
|
||||
<string name="all_permissions_granted">Bütün icazələr verildi 🎉</string>
|
||||
<string name="optional_permission_explanation">Bütün funksiyaların işləməsi üçün əlavə icazələr verməlisiniz</string>
|
||||
<string name="plugins_need_optional_permission">Bəzi qoşmalarda icazə çatışmamazlığı səbəbindən bir sıra imkanlar söndürülmüşdür (daha çox məlumat üçün toxunun)</string>
|
||||
<string name="share_optional_permission_explanation">Faylları qəbul etmək üçün yaddaşa girişə icazə verilməlidirü</string>
|
||||
<string name="telepathy_permission_explanation">İş Masanızdan telefonunuzdakı SMS\'ləri oxumaq və SMS göndərmək üçün SMS\'ə girişə icazə verməlisiniz</string>
|
||||
<string name="telephony_permission_explanation">İş Masanızda telefon zənglərini görmək üçün Zəng Tarixçəsinə və Zəng yığımı vəziyyətinə icazə verməlisiniz</string>
|
||||
<string name="telephony_optional_permission_explanation">Telefon nömrəsi əvəzinə əlaqənin adını görmək üçün Əlaqə Kitabçasına girişə icazə verməlisiniz</string>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Показване на бутони на мишката</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Задаване на ускорение на показалеца</string>
|
||||
<string name="mousepad_scroll_direction_title">Обръщане на посоката на превъртане</string>
|
||||
<string name="gyro_mouse_enabled_title">Активиране на жироскопската мишка</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Чувствителност на жироскопа</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Щракване с ляв бутон</item>
|
||||
<item>Щракване с десен бутон</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Докоснете тук, за да въведете</string>
|
||||
<string name="clear_compose">Изчистване</string>
|
||||
<string name="send_compose">Изпращане</string>
|
||||
<string name="compose_send_title">Текстът е изпратен</string>
|
||||
<string name="open_compose_send">Съставяне на текст</string>
|
||||
<string name="about_kde_about"><h1>За</h1> <p>KDE е световна общност от софтуерни инженери, художници, писатели, преводачи и творци, които са отдадени на <a href=https://www.gnu.org/philosophy/free-sw.html>свободното разработване на софтуер</a>. KDE създава работната среда Plasma, стотици приложения и многобройните софтуерни библиотеки, които ги поддържат.</p> <p>KDE е кооперативно предприятие: нито една отделна организация контролира насоките или продуктите му. Вместо това ние работим заедно, за да постигнем общата цел да създадем най-добрия свободен софтуер в света. Всеки е добре дошъл да се присъедини и да да допринесе</a> за KDE, включително и вие.</p> Посетете <a href=https://www.kde.org/>https://www.kde.org/</a> за повече информация за общността на KDE и за софтуера, който създаваме.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes">" <h1>Докладвайте за грешки или желания</h1> <p>Софтуерът винаги може да бъде подобрен и екипът на KDE е готов да го направи. Въпреки това вие - потребителят - трябва да да ни кажете, когато нещо не работи според очакванията или може да бъде направено по-добре.</p> <p>KDE разполага със система за проследяване на грешки. Посетете <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> или използвайте бутона \"Докладване на грешка\" от екрана за програмата, за да съобщите за грешки.</p> Ако имате предложение за подобрение, тогава можете да използвате системата за проследяване на грешки, за да регистрирате желанието си. Уверете се, че използвате тежестта, наречена \"Wishlist\"."</string>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Mostra els botons del ratolí</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Estableix l\'acceleració de l\'apuntador</string>
|
||||
<string name="mousepad_scroll_direction_title">Inverteix la direcció del desplaçament</string>
|
||||
<string name="gyro_mouse_enabled_title">Activa el ratolí giroscòpic</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Sensibilitat del giroscopi</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Clic esquerre</item>
|
||||
<item>Clic dret</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Toqueu aquí per a teclejar</string>
|
||||
<string name="clear_compose">Neteja</string>
|
||||
<string name="send_compose">Envia</string>
|
||||
<string name="compose_send_title">Títol de l\'enviament</string>
|
||||
<string name="open_compose_send">Redacta text</string>
|
||||
<string name="about_kde_about"><h1>Quant al</h1> <p>El KDE és una comunitat mundial d\'enginyers, artistes, escriptors, traductors i creadors de programari compromesos amb el desenvolupament de <a href=https://www.gnu.org/philosophy/free-sw.html>programari lliure</a>. El KDE produeix l\'entorn d\'escriptori Plasma, centenars d\'aplicacions i moltes biblioteques de programari que els donen suport.</p> <p>El KDE és una empresa en cooperativa: cap entitat controla la seva direcció o els productes. En el seu lloc, treballem junts per a aconseguir l\'objectiu comú de construir el millor programari lliure del món. Tothom hi és benvingut a <a href=https://community.kde.org/Get_Involved>unir-se i contribuir</a> al KDE, inclosos vosaltres.</p> Visiteu <a href=https://www.kde.org/ca/>https://www.kde.org/ca/</a> per a obtenir més informació sobre la comunitat KDE i el programari que produïm.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Informeu dels errors o desitjos</h1> <p>El programari sempre es pot millorar, i l\'equip del KDE està a punt per a fer-ho. No obstant això, l\'usuari, ha de dir-nos quan alguna cosa no funciona com s\'esperava o si podria fer-se millor.</p> <p>El KDE té un sistema de seguiment d\'errors. Per a informar-ne d\'un, visiteu <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> o useu el botó \"Informa d\'un error\" des de la pantalla Quant al.</p> Si teniu un suggeriment de millora, podeu usar el sistema de seguiment d\'errors per a enregistrar el vostre desig. Assegureu-vos d\'usar la severitat anomenada \"Llista de desitjos\" (Wishlist).</string>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="kde_connect">KDE Connect</string>
|
||||
<string name="manifest_label_share">An Gerät senden</string>
|
||||
<string name="foreground_notification_no_devices">Keine bestehenden Verbindungen</string>
|
||||
<string name="foreground_notification_devices">Verbunden mit %s</string>
|
||||
<string name="foreground_notification_send_clipboard">Zwischenablage senden</string>
|
||||
@@ -50,11 +51,23 @@
|
||||
<string name="remotekeyboard_connected">Verbindung zu entfernter Tastatur ist aktiv</string>
|
||||
<string name="remotekeyboard_multiple_connections">Es besteht mehr als eine Verbindungen zu einer entfernten Tastatur. Um Ihre Konfiguration anzupassen, wählen Sie bitte ein Gerät aus</string>
|
||||
<string name="open_mousepad">Ferneingabe</string>
|
||||
<string name="mousepad_info">Bewegen Sie Ihren Finger über den Bildschirm um den Mauszeiger zu bewegen. Tippen Sie auf den Bildschirm, um einen Klick zu simulieren und benutzen Sie entsprechend zwei/drei Finger für einen Rechts-/Mittelklick. Verwenden Sie zwei Finger, um zu Scrollen und einen langen Druck um Objekte zu verschieben. Gyroskop-Maus-Funktionen können Sie in den Modul-Einstellungen aktivieren.</string>
|
||||
<string name="mousepad_keyboard_input_not_supported">Das verbundene Gerät unterstützt keine Tastatureingaben</string>
|
||||
<string name="mousepad_single_tap_settings_title">Aktionsausführung bei Berührung mit einem Finger einstellen</string>
|
||||
<string name="mousepad_double_tap_settings_title">Aktionsausführung bei Berührung mit zwei Fingern einstellen</string>
|
||||
<string name="mousepad_triple_tap_settings_title">Aktionsausführung bei Berührung mit drei Fingern einstellen</string>
|
||||
<string name="mousepad_sensitivity_settings_title">Empfindlichkeit des Touchpads einstellen</string>
|
||||
<string name="mousepad_mouse_buttons_title">Maustasten anzeigen</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Zeigerbeschleunigung einstellen</string>
|
||||
<string name="mousepad_scroll_direction_title">Bildlaufrichtung umkehren</string>
|
||||
<string name="gyro_mouse_enabled_title">Gyroskop-Maus aktivieren</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Empfindlichkeit des Gyroskops einstellen</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Linksklick</item>
|
||||
<item>Rechtsklick</item>
|
||||
<item>Mittelklick</item>
|
||||
<item>Nichts</item>
|
||||
</string-array>
|
||||
<string-array name="mousepad_sensitivity_entries">
|
||||
<item>Langsamste</item>
|
||||
<item>Langsam</item>
|
||||
@@ -74,11 +87,19 @@
|
||||
<string name="sendkeystrokes_textbox_hint">Tastendruck an Rechner senden</string>
|
||||
<string name="sendkeystrokes_disabled_toast">Das Senden von Tastatureingaben ist deaktiviert - aktivieren Sie es in den Einstellungen</string>
|
||||
<string name="sendkeystrokes_wrong_data">Ungültiger MIME-Typ - er muss „text/x-keystrokes“ sein</string>
|
||||
<string name="sendkeystrokes_sent_text">%1$s an Gerät %2$s senden</string>
|
||||
<string name="sendkeystrokes_pref_category_summary">Mit diesem Modul können andere Anwendungen Textsegmente als Tastendrücke teilen, die an den verbundenen Rechner gesendet werden</string>
|
||||
<string name="sendkeystrokes_pref_category_title">Tastendrücke senden</string>
|
||||
<string name="sendkeystrokes_pref_enabled">Senden von Tastendrücken aktivieren</string>
|
||||
<string name="sendkeystrokes_pref_enabled_summary">Auf Daten mit dem MIME-Typ „text/x-keystrokes“ warten</string>
|
||||
<string name="pref_plugin_mousepad_send_keystrokes">Als Tastendruck senden</string>
|
||||
<string name="mouse_receiver_plugin_description">Empfänger für entfernte Mauseingaben</string>
|
||||
<string name="mouse_receiver_plugin_name">Maus-Empfänger</string>
|
||||
<string name="mouse_receiver_no_permissions">Sie müssen den Zugangshilfendienst aktivieren</string>
|
||||
<string name="view_status_title">Status</string>
|
||||
<string name="battery_status_format">Akku: %d%%</string>
|
||||
<string name="battery_status_low_format">Akku: %d%% Niedriger Ladestand</string>
|
||||
<string name="battery_status_charging_format">Akku: %d%% Wird geladen</string>
|
||||
<string name="category_connected_devices">Verbundene Geräte</string>
|
||||
<string name="category_not_paired_devices">Verfügbare Geräte</string>
|
||||
<string name="category_remembered_devices">Gemerkte Geräte</string>
|
||||
@@ -147,6 +168,8 @@
|
||||
<string name="mpris_rew">Schneller Rücklauf</string>
|
||||
<string name="mpris_ff">Vorlauf</string>
|
||||
<string name="mpris_next">Weiter</string>
|
||||
<string name="mpris_loop">Wiederholen</string>
|
||||
<string name="mpris_shuffle">Mischen</string>
|
||||
<string name="mpris_volume">Lautstärke</string>
|
||||
<string name="mpris_time_settings_title">Knöpfe Vorwärts/Rückwärts</string>
|
||||
<string name="mpris_time_settings_summary">Sprungweite für Vorlauf/Rücklauf anpassen</string>
|
||||
@@ -180,6 +203,7 @@
|
||||
<string name="share_received_file">%s freigeben</string>
|
||||
<string name="title_activity_notification_filter">Benachrichtigungs-Filter</string>
|
||||
<string name="filter_apps_info">Benachrichtigungen werden zwischen den ausgewählten Anwendungen abgeglichen</string>
|
||||
<string name="show_notification_if_screen_off">Nur bei ausgeschaltetem Bildschirm Benachrichtigungen senden</string>
|
||||
<string name="add_device_dialog_title">Gerät hinzufügen</string>
|
||||
<string name="add_device_hint">Rechnername oder IP-Adresse</string>
|
||||
<string name="sftp_preference_configured_storage_locations">Vorhandene Speicherort</string>
|
||||
@@ -196,6 +220,8 @@
|
||||
<string name="sftp_saf_permission_explanation">Um von außerhalb auf Ihre Dateien zugreifen zu können, muss mindestens ein Speicherort vorhanden sein</string>
|
||||
<string name="no_players_connected">Keine Medienspieler gefunden</string>
|
||||
<string name="send_files">Dateien senden</string>
|
||||
<string name="block_notification_contents">Benachrichtigungsinhalte blockieren</string>
|
||||
<string name="block_notification_images">Bilder in Benachrichtigungen blockieren</string>
|
||||
<string name="pairing_title">KDE-Connect-Geräte</string>
|
||||
<string name="pairing_description">Andere Geräte im selben Netzwerk, auf denen KDE Connect läuft, sollten hier angezeigt werden</string>
|
||||
<string name="device_rename_title">Geräte umbenennen</string>
|
||||
@@ -218,6 +244,7 @@
|
||||
<string name="close">Schließen</string>
|
||||
<string name="plugins_need_permission">Einige Module benötigen zusätzliche Berechtigungen, tippen Sie für weitere Details:</string>
|
||||
<string name="permission_explanation">Dieses Modul benötigt zusätzliche Berechtigungen</string>
|
||||
<string name="all_permissions_granted">Alle Berechtigungen erteilt 🎉</string>
|
||||
<string name="optional_permission_explanation">Es müssen weitere Berechtigungen erteilt werden, um alle Funktionen nutzen zu können</string>
|
||||
<string name="plugins_need_optional_permission">Einige Module haben eingeschränkte Funktionen wegen fehlender Berechtigungen, tippen Sie für weitere Informationen:</string>
|
||||
<string name="telepathy_permission_explanation">Um SMS vom Rechner aus zu lesen und zu versenden, muss der Zugriff auf die SMS-Funktion gewährt werden</string>
|
||||
@@ -240,6 +267,7 @@
|
||||
<string name="pref_plugin_mprisreceiver">Steuerung der Medienwiedergabe</string>
|
||||
<string name="pref_plugin_mprisreceiver_desc">Ein verbundenes Gerät zum Steuern der lokalen Medienwiedergabe verwenden</string>
|
||||
<string name="notification_channel_default">Andere Benachrichtigungen</string>
|
||||
<string name="notification_channel_persistent">Dauerhafte Benachrichtigung</string>
|
||||
<string name="notification_channel_media_control">Medienkontrolle</string>
|
||||
<string name="notification_channel_filetransfer">Dateiübertragung</string>
|
||||
<string name="notification_channel_high_priority">Hohe Priorität</string>
|
||||
@@ -286,6 +314,7 @@
|
||||
<string name="clipboard_android_x_incompat">In Android 10 wurde der Zugriff auf die Zwischenablage für alle Apps entfernt. Diese Modul wird deaktiviert.</string>
|
||||
<string name="mpris_open_url">Wiedergabe hier fortsetzen</string>
|
||||
<string name="cant_open_url">Die URL zum Fortsetzen der Wiedergabe kann nicht geöffnet werden</string>
|
||||
<string name="bigscreen_home">Startseite</string>
|
||||
<string name="bigscreen_up">Oben</string>
|
||||
<string name="bigscreen_left">Links</string>
|
||||
<string name="bigscreen_select">Auswählen</string>
|
||||
@@ -323,4 +352,25 @@
|
||||
<string name="donate">Spenden</string>
|
||||
<string name="source_code">Quelltext</string>
|
||||
<string name="licenses">Lizenzen</string>
|
||||
<string name="website">Webseite</string>
|
||||
<string name="about">Über</string>
|
||||
<string name="authors">Autoren</string>
|
||||
<string name="thanks_to">Dank an</string>
|
||||
<string name="email_contributor">E-Mail an den Mitwirkenden senden\n%s</string>
|
||||
<string name="visit_contributors_homepage">Internetseite des Mitwirkenden besuchen\n%s</string>
|
||||
<string name="version">Version %s</string>
|
||||
<string name="about_kde">Über KDE</string>
|
||||
<string name="kde_be_free">KDE - Freiheit genießen.</string>
|
||||
<string name="kde">KDE</string>
|
||||
<string name="konqi">Konqi</string>
|
||||
<string name="clear_compose">Leeren</string>
|
||||
<string name="send_compose">Senden</string>
|
||||
<string name="open_compose_send">Text schreiben</string>
|
||||
<string name="maintainer_and_developer">Betreuer und Entwickler</string>
|
||||
<string name="developer">Entwickler</string>
|
||||
<string name="bug_fixes_and_general_improvements">Fehlerbereinigung und allgemeine Verbesserungen</string>
|
||||
<string name="aniket_kumar_task">Verbesserungen am SMS-Modul</string>
|
||||
<string name="alex_fiestas_task">Verbesserungen am Kontakte-Modul</string>
|
||||
<string name="send_clipboard">Zwischenablage senden</string>
|
||||
<string name="tap_to_execute">Tippen um auszuführen</string>
|
||||
</resources>
|
||||
|
@@ -51,13 +51,17 @@
|
||||
<string name="remotekeyboard_connected">Remote keyboard connection is active</string>
|
||||
<string name="remotekeyboard_multiple_connections">There is more than one remote keyboard connection, select the device to configure</string>
|
||||
<string name="open_mousepad">Remote input</string>
|
||||
<string name="mousepad_info">Move a finger on the screen to move the mouse cursor. Tap for a click, and use two/three fingers for right and middle buttons. Use 2 fingers to scroll. Use a long press to drag and drop. Gyro mouse functionality can be enabled from plugin preferences</string>
|
||||
<string name="mousepad_keyboard_input_not_supported">Keyboard input not supported by the paired device</string>
|
||||
<string name="mousepad_single_tap_settings_title">Set one finger tap action</string>
|
||||
<string name="mousepad_double_tap_settings_title">Set two finger tap action</string>
|
||||
<string name="mousepad_triple_tap_settings_title">Set three finger tap action</string>
|
||||
<string name="mousepad_sensitivity_settings_title">Set touchpad sensitivity</string>
|
||||
<string name="mousepad_mouse_buttons_title">Show mouse buttons</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Set pointer acceleration</string>
|
||||
<string name="mousepad_scroll_direction_title">Reverse Scrolling Direction</string>
|
||||
<string name="gyro_mouse_enabled_title">Enable gyroscope mouse</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Gyroscope sensitivity</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Left click</item>
|
||||
<item>Right click</item>
|
||||
@@ -372,6 +376,7 @@
|
||||
<string name="click_here_to_type">Tap here to type</string>
|
||||
<string name="clear_compose">Clear</string>
|
||||
<string name="send_compose">Send</string>
|
||||
<string name="compose_send_title">Compose send</string>
|
||||
<string name="open_compose_send">Compose text</string>
|
||||
<string name="about_kde_about"><h1>About</h1> <p>KDE is a world-wide community of software engineers, artists, writers, translators and creators who are committed to <a href=https://www.gnu.org/philosophy/free-sw.html>Free Software</a> development. KDE produces the Plasma desktop environment, hundreds of applications, and the many software libraries that support them.</p> <p>KDE is a cooperative enterprise: no single entity controls its direction or products. Instead, we work together to achieve the common goal of building the world\'s finest Free Software. Everyone is welcome to <a href=https://community.kde.org/Get_Involved>join and contribute</a> to KDE, including you.</p> Visit <a href=https://www.kde.org/>https://www.kde.org/</a> for more information about the KDE community and the software we produce.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Report Bugs or Wishes</h1> <p>Software can always be improved, and the KDE team is ready to do so. However, you - the user - must tell us when something does not work as expected or could be done better.</p> <p>KDE has a bug tracking system. Visit <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> or use the \"Report Bug\" button from the about screen to report bugs.</p> If you have a suggestion for improvement then you are welcome to use the bug tracking system to register your wish. Make sure you use the severity called \"Wishlist\".</string>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Mostrar botones del ratón</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Establecer la aceleración del puntero</string>
|
||||
<string name="mousepad_scroll_direction_title">Invertir dirección de desplazamiento</string>
|
||||
<string name="gyro_mouse_enabled_title">Activar ratón giroscópico</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Sensibilidad del giroscopio</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Clic izquierdo</item>
|
||||
<item>Clic derecho</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Pulse para escribir</string>
|
||||
<string name="clear_compose">Borrar</string>
|
||||
<string name="send_compose">Enviar</string>
|
||||
<string name="compose_send_title">Componer envío</string>
|
||||
<string name="open_compose_send">Componer texto</string>
|
||||
<string name="about_kde_about"><h1>Acerca de</h1> <p>KDE es una comunidad global de ingenieros software, artistas, escritores, traductores y creadores que siguen el desarrollo de <a href=https://www.gnu.org/philosophy/free-sw.html>Software Libre</a>. KDE produce el entorno de escritorio Plasma, cientos de aplicaciones y todas las librerías en las que se basan.</p> <p>KDE es una empresa colaborativa: no hay una entidad única que controla sus productos o su dirección. En su lugar, trabajamos de manera conjunta para conseguir la meta común de construir el mejor software libre posible. Todo el mundo es bienvenido a <a href=https://community.kde.org/Get_Involved>unirse y contribuir</a> a KDE, incluido usted.</p> Visite <a href=https://www.kde.org/>https://www.kde.org/</a> para más información sobre la comunidad KDE y el software que creamos.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Reporte errores o deseos</h1> <p>El software siempre puede ser mejorado y el equipo de KDE está preparado para ello. Sin embargo, usted - el usuario - debe comunicarnos cuando algo no funciona como es esperado o que puede ser mejorado. </p> <p> KDE tiene un sistema de traqueo de errores. Visite <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> o use el botón «Informar de fallo» en la ventana «Acerca de» para reportar errores.</p> Si tiene una sugerencia de mejora entonces use el sistema de traqueo de errores para registrar su sugerencia. Asegúrese de que usa la severidad «Lista de deseos».</string>
|
||||
|
@@ -51,11 +51,13 @@
|
||||
<string name="remotekeyboard_connected">Etänäppäimistöyhteys on käytössä</string>
|
||||
<string name="remotekeyboard_multiple_connections">Etänäppäimistöyhteyksiä on useampia: valitse asetettava laite</string>
|
||||
<string name="open_mousepad">Kauko-ohjaus</string>
|
||||
<string name="mousepad_info">Siirrä hiirikohdistinta liikuttamalla sormea näytöllä. Tee hiirenpainallus napauttamalla, ja käytä kahta tai kolmea sormea oikealle ja keskipainikkeelle. Vieritä kahdella sormella. Pitkällä painalluksella voit vetää ja pudottaa. Gyrohiiritoiminnon voi ottaa käyttää liitännäisen asetuksista</string>
|
||||
<string name="mousepad_keyboard_input_not_supported">Paritettu laite ei tue näppäimistösyötettä</string>
|
||||
<string name="mousepad_single_tap_settings_title">Aseta yhden sormen napautuksen toiminto</string>
|
||||
<string name="mousepad_double_tap_settings_title">Aseta kahden sormen napautuksen toiminto</string>
|
||||
<string name="mousepad_triple_tap_settings_title">Aseta kolmen sormen napautuksen toiminto</string>
|
||||
<string name="mousepad_sensitivity_settings_title">Aseta kosketuslevyn herkkyys</string>
|
||||
<string name="mousepad_mouse_buttons_title">Näytä hiiripainikkeet</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Aseta osoittimen kiihdytys</string>
|
||||
<string name="mousepad_scroll_direction_title">Käänteinen vierityssuunta</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Afficher les boutons de souris</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Définir l\'accélération du pointeur</string>
|
||||
<string name="mousepad_scroll_direction_title">Inverser la direction du défilement</string>
|
||||
<string name="gyro_mouse_enabled_title">Activer la souris avec gyroscope</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Sensibilité du gyroscope</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Clic gauche</item>
|
||||
<item>Clic droit</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Tapotez ici pour effectuer une saisie</string>
|
||||
<string name="clear_compose">Effacer</string>
|
||||
<string name="send_compose">Envoyer</string>
|
||||
<string name="compose_send_title">Préparer l\'envoi</string>
|
||||
<string name="open_compose_send">Composer du texte</string>
|
||||
<string name="about_kde_about"><h1>A propos</h1> <p>KDE est une communauté mondiale d\'ingénieurs en logiciel, d\'artistes d\'ingénieurs logiciels, d\'artistes, d\'écrivains, de traducteurs et de créateurs s\'engageant pour le développement de <a href=https://www.gnu.org/philosophy/free-sw.html>Logiciels libres</a&gt. KDE développe l\'environnement de bureau Plasma, des centaines d\'applications, et les nombreuses bibliothèques logicielles les prenant en charge. KDE est une entreprise coopérative : aucune entité centrale ne contrôle sa direction ou ses produits. Au contraire, nous travaillons tous ensemble pour atteindre un objectif commun : construire le meilleur logiciel libre au monde. Tout le monde est est le bienvenu pour <a href=https://community.kde.org/Get_Involved>rejoindre et contribuer</a> à KDE, y compris vous. </p> Visitez <a href=https://www.kde.org/>https://www.kde.org/</a> pour de plus amples informations sur la communauté KDE et les logiciels que nous développons.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Signaler des bogues ou des souhaits</h1> <p> Les logiciels peuvent toujours être améliorés et l\'équipe KDE est prête à le faire. Cependant, vous - la personne utilisatrice - devez nous dire quand quelque chose ne fonctionne pas comme prévu ou pourrait être mieux fait. KDE dispose d\'un système de suivi des bogues. Visitez <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> ou utilisez le bouton bouton « Signaler un bogue » de la page « A propos » pour signaler les bogues. Si vous avez une suggestion d\'amélioration, vous pouvez aussi utiliser le système de suivi des bogues pour enregistrer votre souhait. Veuillez vous assurer de bien utiliser le niveau de gravité appelée « Liste de souhaits ».</string>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Mostrar os botóns do rato</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Definir a aceleración do punteiro</string>
|
||||
<string name="mousepad_scroll_direction_title">Inverter a dirección de desprazamento</string>
|
||||
<string name="gyro_mouse_enabled_title">Activar o rato de xiroscopio</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Sensibilidade de xiroscopio</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Clic esquerdo</item>
|
||||
<item>Clic dereito</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Toque aquí para escribir</string>
|
||||
<string name="clear_compose">Borrar</string>
|
||||
<string name="send_compose">Enviar</string>
|
||||
<string name="compose_send_title">Preparar un envío</string>
|
||||
<string name="open_compose_send">Escribir texto</string>
|
||||
<string name="about_kde_about">"<h1>Sobre</h1> <p>KDE é unha comunidade internacional de persoas adicadas á enxeñaría de software, á arte, á documentación, á tradución e á creación, todas elas comprometidas co desenvolvemento de <a href=https://www.gnu.org/philosophy/free-sw.html>software libre</a>. KDE produce o ambiente de escritorio Plasma, centos de aplicacións, e as moitas bibliotecas de software sobre as que estas están construídas.</p> <p>KDE é un esforzo cooperativo: non hai unha única entidade que controle a súa dirección ou os seus produtos. No seu lugar, xuntámonos para traballar no obxectivo común de construír o mellor software libre do mundo. Todas as persoas son benvidas a <a href=https://community.kde.org/Get_Involved>unirse e colaborar</a> en KDE, incluída vostede.</p> Visite <a href=https://www.kde.org/>https://www.kde.org/</a> para máis información sobre a comunidade KDE e o software que produce."</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Informe de fallos ou pida melloras</h1> <p>O software sempre pode mellorarse, e o equipo de KDE está preparado para facelo. Porén, vostede, a persoa usuaria, ten que avisarnos cando algo non funciona como espera ou podería mellorarse.</p> <p>KDE ten un sistema de seguimento de fallos. Visite <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> ou use o botón de «Informar dun fallo» da pantalla de información para informar dun fallo.</p> Se ten unha suxestión de mellora tamén pode usar o sistema de seguimento de fallos para rexistrala. Asegúrese nese caso de usar a severidade «Lista de desexos».</string>
|
||||
|
@@ -185,6 +185,7 @@
|
||||
<string name="click_here_to_type">Tocca hic pro typar</string>
|
||||
<string name="clear_compose">Clara</string>
|
||||
<string name="send_compose">Invia</string>
|
||||
<string name="compose_send_title">Compone invia</string>
|
||||
<string name="open_compose_send">Compone texto</string>
|
||||
<string name="maintainer_and_developer">Mantenitor e developpator</string>
|
||||
<string name="developer">Disveloppator</string>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Mostra i pulsanti del mouse</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Imposta accelerazione del puntatore</string>
|
||||
<string name="mousepad_scroll_direction_title">Inverti direzione di scorrimento</string>
|
||||
<string name="gyro_mouse_enabled_title">Abilita il mouse giroscopico</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Sensibilità del giroscopio</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Clic sinistro</item>
|
||||
<item>Clic destro</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Tocca qui per scrivere</string>
|
||||
<string name="clear_compose">Pulisci</string>
|
||||
<string name="send_compose">Invia</string>
|
||||
<string name="compose_send_title">Invio scorciatoia composita</string>
|
||||
<string name="open_compose_send">Componi il testo</string>
|
||||
<string name="about_kde_about"><h1>Informazioni</h1> <p>KDE è una comunità mondiale di ingegneri del software, artisti, scrittori, traduttori e creatori che si impegnano a sviluppare <a href=https://www.gnu.org/philosophy/free-sw.html>software libero</a>. KDE produce l\'ambiente desktop Plasma, centinaia di applicazioni e le numerose librerie software che le supportano.</p> <p>KDE è un\'impresa cooperativa: nessuna singola entità ne controlla la direzione o i prodotti. Invece, lavoriamo insieme per raggiungere l\'obiettivo comune di costruire il miglior software libero del mondo. Tutti sono invitati a <a href=https://community.kde.org/Get_Involved>unirsi e contribuire</a> a KDE, incluso te.</p> Visita <a href=https://www.kde.org/>https://www.kde.org/</a> per ulteriori informazioni sulla comunità KDE e sul software che produciamo.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Segnala bug o desideri</h1> <p>Il software può sempre essere migliorato e il team di KDE è pronto a farlo. Tuttavia, tu - l\'utente - devi dirci quando qualcosa non funziona come previsto o potrebbe essere fatto meglio.</p> <p>KDE ha un sistema di tracciamento dei bug. Visita <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> oppure utilizza il pulsante «Segnala bug» dalla schermata delle informazioni per segnalare i bug.</p> Se hai un suggerimento per il miglioramento, puoi utilizzare il sistema di tracciamento dei bug per registrare il tuo desiderio. Assicurati di utilizzare «Wishlist» per il campo Severity.</string>
|
||||
|
@@ -45,6 +45,8 @@
|
||||
<string name="mousepad_sensitivity_settings_title">დააყენეთ თაჩპედის მგრძნობიარობა</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">კურსორის აჩქარების დაყენება</string>
|
||||
<string name="mousepad_scroll_direction_title">აწევ-ჩამოწევის მიმართულების რევერსი</string>
|
||||
<string name="gyro_mouse_enabled_title">გიროსკოპის თაგუნას დაჩართვა</string>
|
||||
<string name="gyro_mouse_sensitivity_title">გიროსკოპის მგრძნობელობა</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>მარცხენა-წკაპი</item>
|
||||
<item>მარჯვენა წკაპი</item>
|
||||
@@ -66,6 +68,7 @@
|
||||
<item>ძლიერი</item>
|
||||
<item>უძლიერესი</item>
|
||||
</string-array>
|
||||
<string name="sendkeystrokes_sent_text">%1$s გაგზავნილია მოწყობილობაზე %2$s</string>
|
||||
<string name="sendkeystrokes_pref_category_title">სად გაიგზავნება ღილაკის დაჭერა</string>
|
||||
<string name="sendkeystrokes_pref_enabled">ღილაკის დაჭერის გაგზავნის ჩართვა</string>
|
||||
<string name="mouse_receiver_plugin_name">თაგუნას მიმღები</string>
|
||||
@@ -274,6 +277,7 @@
|
||||
<string name="click_here_to_type">ასაკრეფად აქ დაატყაპუნეთ</string>
|
||||
<string name="clear_compose">გაწმენდა</string>
|
||||
<string name="send_compose">გაგზავნა</string>
|
||||
<string name="compose_send_title">შედგენის გაგზავნა</string>
|
||||
<string name="open_compose_send">ტექსტის შედგენა</string>
|
||||
<string name="maintainer_and_developer">პროგრამისტი და წამყვანი</string>
|
||||
<string name="developer">პროგრამისტი</string>
|
||||
|
@@ -11,7 +11,6 @@
|
||||
<color name="toolbar_color">@android:color/system_neutral1_900</color>
|
||||
<color name="card_stroke_color">@android:color/system_neutral2_800</color>
|
||||
<color name="activity_background">@android:color/system_neutral1_900</color>
|
||||
<item name="lightMode" type="bool">false</item>
|
||||
|
||||
<!-- This is for dark theme. In dark theme both selected and unselected text in the
|
||||
navigation bar should be white. This is different from the light theme as both states have
|
||||
|
@@ -11,7 +11,6 @@
|
||||
<color name="toolbar_color">@android:color/black</color>
|
||||
<color name="card_stroke_color">#8C8C8C</color>
|
||||
<color name="activity_background">@android:color/black</color>
|
||||
<item name="lightMode" type="bool">false</item>
|
||||
|
||||
<!-- This is for dark theme. In dark theme both selected and unselected text in the
|
||||
navigation bar should be white. This is different from the light theme as both states have
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Muisknoppen tonen</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Aanwijzerversnelling instellen</string>
|
||||
<string name="mousepad_scroll_direction_title">Schuifrichting omdraaien</string>
|
||||
<string name="gyro_mouse_enabled_title">Gyroscoopmuis inschakelen</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Gyroscoopgevoeligheid</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Linker muisklik</item>
|
||||
<item>Rechter muisklik</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Tik hier om te typen</string>
|
||||
<string name="clear_compose">Wissen</string>
|
||||
<string name="send_compose">Verzenden</string>
|
||||
<string name="compose_send_title">Opstellen van verzending</string>
|
||||
<string name="open_compose_send">Tekst opstellen</string>
|
||||
<string name="about_kde_about"><h1>Info over</h1> <p>KDE is een wereldwijde gemeenschap van software ingenieurs, artiesten, schrijvers, vertalers en makers die toegewijd zijn aan <a href=https://www.gnu.org/philosophy/free-sw.html>Vrije software</a> ontwikkeling. KDE produceert de Plasma bureaubladomgeving, honderden toepassingen en de vele software bibliotheken die deze ondersteunen.</p> <p>KDE is een coöperatieve onderneming: geen enkele entiteit controleert zijn richting of producten. In plaats daarvan werken we samen om het gemeenschappelijke doel te bereiken van het bouwen van de \'s werelds mooiste Vrije software. Iedereen is welkom om <a href=https://community.kde.org/Get_Involved>mee te doen en bij te dragen</a> aan KDE, inclusief u.</p> Bezoek <a href=https://www.kde.org/>https://www.kde.org/</a> voor meer informatie over de KDE gemeenschap en de software die we produceren.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Bugs of wensen rapporteren</h1> <p>Software kan altijd verbeterd worden en het KDE team is gereed om dat te doen. Echter, u - de gebruiker - moet ons vertellen wanneer iets niet werkt zoals verwacht of beter gedaan kan worden.</p> <p>KDE heeft een bugvolgsysteem. Bezoek <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> of gebruik de knop \"Bug rapporteren\" uit het Info over scherm om bugs te rapporteren.</p> Als u een suggestie voor verbetering dan bent u welkom om het bugvolgsysteem te gebruiken om uw wens te registreren. Ga na dat u de ernst genaamd \"Wishlist\" gebruikt.</string>
|
||||
|
@@ -51,13 +51,17 @@
|
||||
<string name="remotekeyboard_connected">Eksternt tastatursamband er verksamt</string>
|
||||
<string name="remotekeyboard_multiple_connections">Det finst meir enn eitt eksternt tastatursamband (vel eining å setja opp)</string>
|
||||
<string name="open_mousepad">Fjernstyring</string>
|
||||
<string name="mousepad_info">Dra ein finger over skjermen for å flytta peikaren på datamaskina. Trykk for å klikka, og bruk to eller tre fingrar for høvesvis høgre- og midtknappen. Bruk to fingrar for å rulla. Trykk lenge for å dra og sleppa. Funksjonalitet for gyromus kan du slå på i programtillegg-innstillingane.</string>
|
||||
<string name="mousepad_keyboard_input_not_supported">Tekst frå tastaturet er ikkje støtta av den para eininga</string>
|
||||
<string name="mousepad_single_tap_settings_title">Vel handling for éinfingertrykk</string>
|
||||
<string name="mousepad_double_tap_settings_title">Vel handling for tofingertrykk</string>
|
||||
<string name="mousepad_triple_tap_settings_title">Vel handling for trefingertrykk</string>
|
||||
<string name="mousepad_sensitivity_settings_title">Vel følsemd for styreplate</string>
|
||||
<string name="mousepad_mouse_buttons_title">Vis museknappar</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Vel peikarakselerasjon</string>
|
||||
<string name="mousepad_scroll_direction_title">Omvend rulleretning</string>
|
||||
<string name="gyro_mouse_enabled_title">Slå på gyromus</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Gyroskop-følsemd</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Venstreklikk</item>
|
||||
<item>Høgreklikk</item>
|
||||
|
@@ -11,6 +11,5 @@
|
||||
<color name="toolbar_color">@android:color/system_neutral1_50</color>
|
||||
<color name="card_stroke_color">@android:color/system_neutral2_100</color>
|
||||
<color name="activity_background">@android:color/system_neutral1_50</color>
|
||||
<item name="lightMode" type="bool">true</item>
|
||||
</resources>
|
||||
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Pokaż przyciski myszy</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Ustaw przyspieszenie wskaźnika</string>
|
||||
<string name="mousepad_scroll_direction_title">Odwróć stronę przewijania</string>
|
||||
<string name="gyro_mouse_enabled_title">Włącz mysz żyroskopową</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Czułość żyroskopu</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Kliknięcie lewym</item>
|
||||
<item>Kliknięcie prawym</item>
|
||||
@@ -390,6 +392,7 @@
|
||||
<string name="click_here_to_type">Stuknij tutaj, aby wpisać</string>
|
||||
<string name="clear_compose">Wyczyść</string>
|
||||
<string name="send_compose">Wyślij</string>
|
||||
<string name="compose_send_title">Napisz do wysłania</string>
|
||||
<string name="open_compose_send">Napisz tekst</string>
|
||||
<string name="about_kde_about">"<h1>O programie</h1> <p>KDE to światowa społeczność inżynierów oprogramowania, artystów, pisarzy, tłumaczy i twórców, którzy są częścią rozwoju <a href=https://www.gnu.org/philosophy/free-sw.html>Wolnego Oprogramowania</a>. KDE tworzy środowisko pulpitu Plazmy, setki aplikacji i wiele bibliotek programistycznych, aby je wspierać.</p> <p>KDE jest przedsięwzięciem istniejącym ze współpracy; jego ruchami, czy produktami, nie steruje żaden pojedynczy byt. Pracujemy razem, aby osiągnąć wspólny cel, czyli budowę najlepszego Wolnego Oprogramowania na świecie. Każdy jest mile wiedziany, żeby <a href=https://community.kde.org/Get_Involved>dołączył i zaczął współtworzyć</a> KDE, włączając w to ciebie.</p> Odwiedź < href=https://www.kde.org/>https://www.kde.org/</a> po więcej szczegółów nt. społeczności KDE i oprogramowania, które tworzymy."</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Zgłaszaj błędy lub życzenia</h1> <p>Oprogramowanie zawsze można ulepszyć, a zespół KDE jest gotowy, aby to robić. Jednakże ty - użytkownik - musisz nam powiedzieć o tym, co nie działa jak powinno lub co można zrobić lepiej.</p> <p>KDE ma system obsługi błędów. Odwiedź <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> lub użyj przycisku \"Zgłoś błąd\" z ekranu o programie do zgłaszania błędów.</p> Jeśli masz sugestie nt. usprawnień, to także możesz ją zarejestrować w naszym systemie obsługi błędów. Upewnij się, że użyjesz ważności o nazwie \"Lista życzeń\".</string>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Mostrar os botões do rato</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Definir a aceleração do cursor</string>
|
||||
<string name="mousepad_scroll_direction_title">Direcção de Deslocamento Inversa</string>
|
||||
<string name="gyro_mouse_enabled_title">Activar o rato com giroscópio</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Sensibilidade do giroscópio</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Botão esquerdo</item>
|
||||
<item>Botão direito</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Toque aqui para abrir</string>
|
||||
<string name="clear_compose">Limpar</string>
|
||||
<string name="send_compose">Enviar</string>
|
||||
<string name="compose_send_title">Compor o envio</string>
|
||||
<string name="open_compose_send">Compor o texto</string>
|
||||
<string name="about_kde_about"><h1>Acerca</h1> <p>O KDE é uma grande comunidade mundial de engenheiros de \'software\', artistas, tradutores e criadores comprometidos com o desenvolvimento de <a href=https://www.gnu.org/philosophy/free-sw.html>Software Livre</a>. O KDE produz o ambiente de trabalho Plasma, centenas de aplicações e as diversas bibliotecas de \'software\' que dão suporte a elas.</p> <p>O KDE é uma empresa cooperativa: nenhuma entidade única controla a sua direcção ou produtos. Em vez disso, trabalhamos juntos para atingir o objectivo comum de criar o melhor Software Livre do mundo. Todos são bem-vindos para se <a href=https://community.kde.org/Get_Involved>juntarem e contribuírem</a> para o KDE, incluindo você mesmo.</p> Vá a <a href=https://www.kde.org/>https://www.kde.org/</a> para obter mais informações sobre a comunidade do KDE e as aplicações que produzimos.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Comunicar Erros ou Pedidos</h1> <p>O software pode ser sempre melhorado, e a equipa do KDE está preparada para o fazer. Contudo, você - o utilizador - deve--nos avisar quando algo não funciona como seria de esperar ou quando poderá ser feito de melhor maneira.</p> <p>O KDE tem um sistema de registo de erros. Vá a <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> ou use o botão \"Comunicar um Erro\" do ecrã \'Acerca\' para relatar erros.</p> Se tiver uma sugestão de melhorias, então é bem-vindo para usar o sistema de registo de erros para registar o seu pedido. Certifique-se que usa o tipo de criticidade \"Wishlist\" (Lista de Pedidos).</string>
|
||||
|
@@ -51,6 +51,7 @@
|
||||
<string name="remotekeyboard_connected">Povezava z oddaljeno tipkovnico je dejavna</string>
|
||||
<string name="remotekeyboard_multiple_connections">Obstaja več kot ena povezava na oddaljeno tipkovnico, izberite napravo in nastavitve</string>
|
||||
<string name="open_mousepad">Oddaljeni input</string>
|
||||
<string name="mousepad_info">Premaknite prst po zaslonu da bi premikali miško. Tapnite za klik in uporabite dva/tri prste za desni in srednji gumb. Za pomikanje uporabite dva prsta. Uporabite dolg pritisk za povleci in spusti. Žiroskopsko zmožnost miške lahko omogočite iz nastavitve vtičnika</string>
|
||||
<string name="mousepad_keyboard_input_not_supported">Vnos s tipkovnice ni podprt na sparjeni napravi</string>
|
||||
<string name="mousepad_single_tap_settings_title">Nastavite akcijo tapkanja enega prsta</string>
|
||||
<string name="mousepad_double_tap_settings_title">Nastavite akcijo tapkanja dveh prstov</string>
|
||||
@@ -59,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Prikaži miškine gumbe</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Nastavi pospeške kazalca</string>
|
||||
<string name="mousepad_scroll_direction_title">Smer povratnega drsenja</string>
|
||||
<string name="gyro_mouse_enabled_title">Omogoči žiroskopsko miško</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Občutljivost žiroskopa</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Levi klik</item>
|
||||
<item>Desni klik</item>
|
||||
@@ -389,6 +392,7 @@
|
||||
<string name="click_here_to_type">Tapkajte, tukaj za tipkanje</string>
|
||||
<string name="clear_compose">Očisti</string>
|
||||
<string name="send_compose">Pošlji</string>
|
||||
<string name="compose_send_title">Sestavite besedilo</string>
|
||||
<string name="open_compose_send">Sestavite besedilo</string>
|
||||
<string name="about_kde_about"><h1>O programu</h1> <p>KDE je svetovna skupnost programskih inženirjev, umetnikov, piscev, prevajalcev in ustvarjalcev, ki so podporniki razvoja <a href=https://www.gnu.org/philosophy/free-sw.html>Prostega programja</a>. KDE izdeluje Namizno okolje Plasma, stotine aplikacij in veliko programskih knjižnic, ki jih podpirajo. </p> <p>KDE je zadružno podjetje: noben deležnik posamično ne obvladuje smeri razvoja ali izdelka. Namesto tega delamo skupaj, da bi dosegli skupni cilj razvoja, da bi prišli do najboljšega prostega programja na svetu. Vsakdo je dobrodošel v skupnost <a href=https://community.kde.org/Get_Involved>da se pridruži in prispeva </a> v KDE vključno z vami. </p> Obiščite <a href=https://www.kde.org/>https://www.kde.org/</a> za več informacij o skupnosti KDE community in programju, ki ga razvijamo.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Poročajte o napakah in željah</h1> <p>Programje je vedno mogoče izboljšati in skupina KDE je vedno pripravljena na izboljšave. Vendar, vi - kot uporabnica ali uporabnik - nam morate povedati, kadar nekaj ne deluje kot pričakujete ali bi bilo lahko izdelano bolje. </p> <p>KDE ima sistem sledenja napak. Obiščite <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> ali uporabite gumb \"Poročaj o napaki\" na zaslonu O programu za poročanje napak.</p> Če imate sugestijo za izboljšanje ste povabljeni, da uporabite sistem za sledenje napakam in zabeležite vašo željo. Zagotovite, da boste resnost napake označili kot \"Seznam želja - Wishlist\".</string>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">சுட்டி பட்டன்களைக் காட்டு</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">சுட்டிக்குறியின் வேகவளர்ச்சியை அமை</string>
|
||||
<string name="mousepad_scroll_direction_title">உருளல் திசையை புரட்டு</string>
|
||||
<string name="gyro_mouse_enabled_title">சுழல்காட்டி சுட்டியை இயக்கு</string>
|
||||
<string name="gyro_mouse_sensitivity_title">சுழல்காட்டியின் உணர்வுத்திறம்</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>இடது கிளிக்</item>
|
||||
<item>வலது கிளிக்</item>
|
||||
@@ -371,6 +373,7 @@
|
||||
<string name="click_here_to_type">எழுத இங்கு தட்டுங்கள்</string>
|
||||
<string name="clear_compose">காலியாக்கு</string>
|
||||
<string name="send_compose">அனுப்பு</string>
|
||||
<string name="compose_send_title">உரையை இயற்றி அனுப்பு</string>
|
||||
<string name="open_compose_send">உரையை இயற்று</string>
|
||||
<string name="about_kde_about">"<h1>பற்றி</h1> <p>கே.டீ.யீ. என்பது, <a href=https://www.gnu.org/philosophy/free-sw.html>கட்டற்ற மென்பொருள்</a> உருவாக்கத்திற்கு அர்பணிப்புக் கொண்ட மென்பொருள் பொறிஞர்கள், கலைஞர்கள், எழுத்தாளர்கள், மொழிபெயர்ப்பாளர்கள் மற்றும் படைப்பாலிகளைக் கொண்ட உலகளாவிய குழு ஆகும். பிளாஸ்மா பணிமேடை சூழல், நூற்றுக்கணக்கானசெயலிகள், மற்றும் அவற்றை ஆதரிக்கும் பல நிரலகங்களை கே.டீ.யீ. உருவாக்குகிறது.</p> <p>கே.டீ.யீ. ஒரு கூட்டுறவு அமைப்பாகும்: எந்தவொரு தனிப்பட்ட நிறுவனமோ நபரோ அதன் நோக்கத்தையோ படைப்புக்களையோ கட்டுப்படுத்துவதில்லை. கே.டீ.யீ.-யில் நீங்கள் உட்பட எவரேனும் <a href=https://community.kde.org/Get_Involved>இணைந்து பங்களிக்கலாம்</a>. </p> கே.டீ.யீ. சமூகத்தை பற்றியும் நாங்கள் உருவாக்கும் மென்பொருட்களை பற்றியும் அறிய <a href=https://www.kde.org/>https://www.kde.org/</a> என்ற பக்கத்தை அணுகுங்கள்."</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>பிழைகளையோ விருப்பங்களையோ தாக்கல் செய்யுங்கள்</h1> <p>எந்த மென்பொருளும் மேம்படுத்த தக்கதே. கே.டீ.யீ. குழு அதனைச் செய்ய தயாராக உள்ளது. ஆயினும் பயனராகிய நீங்கள், எதிர்பார்த்த படி பணிசெய்யாதவை குறித்தும், இன்னும் சிறப்பாகச் செய்யக்கூடியவை குறித்தும் எங்களுக்கு தெரியப்படுத்த வேண்டும். </p> <p>கே.டீ.யீ.க்கு, பிழைகளை கண்காணிக்கும் அமைப்பொன்று உள்ளது. <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> என்ற பக்கத்தை அணுகவும், அல்லது \"உதவி\" பட்டியிலுள்ள \"பிழையைத் தெரிவி...\" என்ற சாளரத்தை பயன்படுத்தவும்.</p> நீங்கள் விரும்பும் மாற்றங்களை பரிந்துரைக்கக் கூட பிழைகளை கண்காணிக்கும் அமைப்பினைப் பயன்படுத்தலாம். அப்படி தெரிவிக்கும்போது, \"Wishlist\" என்ற முக்கியத்துவத்தை தேர்ந்தெடுங்கள்.</string>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Fare düğmelerini göster</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">İşaretçi ivmesini ayarla</string>
|
||||
<string name="mousepad_scroll_direction_title">Sarma Yönünü Tersine Çevir</string>
|
||||
<string name="gyro_mouse_enabled_title">Jiroskop fareyi etkinleştir</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Jiroskop hassasiyeti</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Sol tık</item>
|
||||
<item>Sağ tık</item>
|
||||
@@ -374,6 +376,7 @@
|
||||
<string name="click_here_to_type">Yazmak için buraya dokunun</string>
|
||||
<string name="clear_compose">Temiz</string>
|
||||
<string name="send_compose">Gönder</string>
|
||||
<string name="compose_send_title">Gönderi oluştur</string>
|
||||
<string name="open_compose_send">Metin oluştur</string>
|
||||
<string name="about_kde_about"><h1>Hakkında</h1> <p>KDE, <a href=https://www.gnu.org/philosophy/free-sw.html>Özgür Yazılım</a> hareketine destek veren yazılım mühendislerinin, sanatçıların, yazarların, çevirmenlerin ve yaratıcıların bir araya geldiği dünya çapında bir topluluktur KDE, Plasma masaüstü ortamını, yüzlerce uygulamayı ve onları destekleyen sayısız yazılım kitaplığını üretir.</p> <p>KDE, işbirlikçi bir kurumdur: Tek bir varlık yönünü veya ürünlerini kontrol etmez. Bunun yerine, dünyanın en kaliteli Özgür Yazılım\'larını üretme hedefi için birlikte çalışırız. Herkes, sen de dahil olmak üzere, KDE\'ye <a href=https://community.kde.org/Get_Involved>katılıp katkıda bulunmakta özgürdür</a>.</p> KDE topluluğu ve ürettiğimiz yazılımlar hakkında daha fazla bilgi için <a href=https://www.kde.org/>https://www.kde.org/</a> adresini ziyaret edin.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Hataları veya İsteklerinizi Bildirin</h1> <p>Yazılım her zaman iyileştirilebilir ve KDE takımın bunu yapmaya hazır. Ancak siz de bir şey beklendiği gibi gitmezse veya hata verirse bize bildirin.</p> <p>KDE\'nin bir hata takip sistemi vardır. <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> adresini ziyaret edin veya hakkında ekranının \"Hata Bildir\" düğmesini kullanarak hataları bildirin.</p> Bir iyileştirme için öneriniz varsa bunu bildirmek için hata takip sistemini kullanabilirsiniz; yalnızca \"Wishlist\" ciddiyet düzeyini kullandığınızdan emin olun.</string>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">Показати кнопки миші</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">Встановити прискорення вказівника</string>
|
||||
<string name="mousepad_scroll_direction_title">Зворотний напрямок гортання</string>
|
||||
<string name="gyro_mouse_enabled_title">Увімкнути гіроскопічну мишу</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Чутливість гіроскопа</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Клацання лівою</item>
|
||||
<item>Клацання правою</item>
|
||||
@@ -390,6 +392,7 @@
|
||||
<string name="click_here_to_type">Торкніться тут, щоб почати введення</string>
|
||||
<string name="clear_compose">Спорожнити</string>
|
||||
<string name="send_compose">Надіслати</string>
|
||||
<string name="compose_send_title">Надсилання редагованого</string>
|
||||
<string name="open_compose_send">Редагувати текст</string>
|
||||
<string name="about_kde_about"><h1>Інформація</h1> <p>KDE — це всесвітня спільнота програмістів, художників, авторів текстів, перекладачів та фахівців з полегшення користування програмами, які роблять свій внесок до розвитку <a href=https://www.gnu.org/philosophy/free-sw.html>вільного програмного забезпечення</a>. KDE створено стільничне середовище Плазма, сотні вільних програм і багато бібліотек, які є їхньою основою.</p> <p>Розробка KDE є спільною працею, у якій жоден з учасників не має переважного контролю над зусиллями або результатами роботи інших розробників KDE. Ми працюємо разом заради спільної мети — створення найкращого вільного програмного забезпечення. Кожен може <a href=https://community.kde.org/Get_Involved>долучитися і зробити свій внесок</a>, зокрема це можете зробити ви.</p> Відвідайте сайт <a href=https://www.kde.org/>https://www.kde.org/</a>, щоб дізнатися більше про спільноту KDE та створене нею програмне забезпечення.</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>Повідомляйте про вади і ваші побажання</h1> <p>Програмне забезпечення завжди потребує вдосконалення, і команда KDE готова це робити. Проте, вам (користувачеві) варто повідомити нам, якщо щось не працює, як слід, або щось можна покращити.</p> <p>KDE має систему стеження за вадами. Завітайте на сторінку <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> , щоб повідомити розробників про ваду у програмі.</p>Якщо у вас є пропозиція щодо вдосконалення, за допомогою цієї системи можна зареєструвати ваше побажання. Переконайтеся, що поле «Важливість» встановлено у значення «Список побажань» («Wishlist»).</string>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
|
||||
<style name="KdeConnectTheme.NoActionBar" parent="KdeConnectThemeBase.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">@bool/lightMode</item>
|
||||
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="KdeConnectThemeBase.V27" parent="KdeConnectThemeBase">
|
||||
<item name="android:navigationBarColor">@color/activity_background</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:windowLightNavigationBar">?attr/isLightTheme</item>
|
||||
</style>
|
||||
|
@@ -60,6 +60,8 @@
|
||||
<string name="mousepad_mouse_buttons_title">显示鼠标按钮</string>
|
||||
<string name="mousepad_acceleration_profile_settings_title">设置指针加速度</string>
|
||||
<string name="mousepad_scroll_direction_title">反转滚动方向</string>
|
||||
<string name="gyro_mouse_enabled_title">启用陀螺仪鼠标</string>
|
||||
<string name="gyro_mouse_sensitivity_title">陀螺仪灵敏度</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>左键点击</item>
|
||||
<item>右键点击</item>
|
||||
@@ -366,6 +368,7 @@
|
||||
<string name="click_here_to_type">轻触此处输入</string>
|
||||
<string name="clear_compose">清除</string>
|
||||
<string name="send_compose">发送</string>
|
||||
<string name="compose_send_title">编写发送</string>
|
||||
<string name="open_compose_send">编写文本</string>
|
||||
<string name="about_kde_about"><h1>关于</h1> <p>KDE 是由一群致力于<a href=https://www.gnu.org/philosophy/free-sw.html>自由软件</a>事业的人们所组成的全球性协作社区。它的成员包括了来自世界各地的软件工程师、艺术工作者、文字工作者、翻译人员和其他创意人员。KDE 社区开发了 Plasma 桌面环境、数百款功能各异的应用软件、以及用于支持它们的大量程序库。</p> <p>KDE 是一项立足于协作精神的事业,它的运作和产出不受任何单一个人或者机构的控制。我们的共同目标是为全世界带来高品质的自由软件。不管您来自何方,我们都欢迎您<a href=https://community.kde.org/Get_Involved>加入 KDE 并做出贡献</a>。</p>请访问 <a href=https://www.kde.org/>https://www.kde.org/</a> 来了解 KDE 社区和软件的更多信息。</string>
|
||||
<string name="about_kde_report_bugs_or_wishes"><h1>报告程序缺陷和需求</h1> <p>KDE 团队一直致力于改进软件的品质。为了做到这一点,倾听来自用户的反馈非常重要。如果您遇到了软件不能正常工作的情况,请务必告诉我们。如果您有关于改进软件的想法,也请与我们分享。</p> <p>KDE 建有程序缺陷跟踪系统,请访问 <a href=https://bugs.kde.org/>https://bugs.kde.org/</a> 或者使用“帮助”菜单中的“报告缺陷”对话框填写报告。</p>如果您想要提出改进建议而不是报告程序缺陷,请确保在表格的 Severity (严重程度) 选单中选择“Wishlist (需求)”。</string>
|
||||
|
@@ -11,5 +11,4 @@
|
||||
<color name="toolbar_color">@android:color/white</color>
|
||||
<color name="card_stroke_color">#C8C8C8</color>
|
||||
<color name="activity_background">@android:color/white</color>
|
||||
<item name="lightMode" type="bool">true</item>
|
||||
</resources>
|
@@ -75,6 +75,9 @@
|
||||
<string name="mousepad_scroll_direction" translatable="false">mousepad_scroll_direction</string>
|
||||
<string name="gyro_mouse_enabled" translatable="false">gyro_mouse_enabled</string>
|
||||
<string name="mousepad_mouse_buttons_enabled_pref" translatable="false">mouse_buttons_enabled</string>
|
||||
<string name="gyro_mouse_enabled_title">Enable gyroscope mouse</string>
|
||||
<string name="gyro_mouse_sensitivity_title">Gyroscope sensitivity</string>
|
||||
<string name="gyro_mouse_sensitivity" translatable="false">gyro_mouse_sensitivity</string>
|
||||
<string-array name="mousepad_tap_entries">
|
||||
<item>Left click</item>
|
||||
<item>Right click</item>
|
||||
@@ -485,6 +488,7 @@
|
||||
<string name="click_here_to_type">Tap here to type</string>
|
||||
<string name="clear_compose">Clear</string>
|
||||
<string name="send_compose">Send</string>
|
||||
<string name="compose_send_title">Compose send</string>
|
||||
<string name="open_compose_send">Compose text</string>
|
||||
|
||||
<string name="about_kde_about"><![CDATA[
|
||||
|
@@ -2,25 +2,24 @@
|
||||
<!-- NoActionBar because we use a Toolbar widget as ActionBar -->
|
||||
<style name="KdeConnectThemeBase" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- The main color attributes -->
|
||||
<!-- The three colors used by system widgets, according to https://chris.banes.me/2014/10/17/appcompat-v21/ -->
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primaryDark</item>
|
||||
<item name="colorSecondary">@color/primary</item>
|
||||
<item name="colorOnSecondary">@color/on_secondary</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="colorHighContrast">@color/on_high_contrast</item>
|
||||
<item name="android:windowBackground">@color/activity_background</item>
|
||||
<item name="android:colorBackground">@color/activity_background</item>
|
||||
<!-- TODO: The 2 items below change too much (eg snackbar text is now black, should be white) -->
|
||||
<item name="android:textColorPrimary">@color/text_color_primary</item>
|
||||
<item name="android:textColor">@color/text_color</item>
|
||||
|
||||
<!--For android below 23 api-->
|
||||
<item name="android:statusBarColor">@android:color/black</item>
|
||||
|
||||
<!-- Drawable definitions and overrides -->
|
||||
<item name="divider">?colorHighContrast</item>
|
||||
|
||||
<!-- Style overrides -->
|
||||
<item name="actionModeStyle">@style/ActionModeStyle</item>
|
||||
<item name="toolbarStyle">@style/KdeConnectTheme.Toolbar</item>
|
||||
<item name="actionModeStyle">@style/Widget.Material3.ActionMode</item>
|
||||
<item name="toolbarStyle">@style/Widget.Material3.Toolbar</item>
|
||||
|
||||
<!-- Theme overrides -->
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
|
||||
|
@@ -61,7 +61,14 @@
|
||||
android:id="@+id/gyro_mouse_enabled"
|
||||
android:defaultValue="false"
|
||||
android:key="@string/gyro_mouse_enabled"
|
||||
android:title="Gyro mouse" />
|
||||
android:title="@string/gyro_mouse_enabled_title" />
|
||||
|
||||
<SeekBarPreference
|
||||
android:id="@+id/mousepad_gyro_sensitivity"
|
||||
android:defaultValue="100"
|
||||
android:key="@string/gyro_mouse_sensitivity"
|
||||
android:title="@string/gyro_mouse_sensitivity_title"
|
||||
android:layout_width="wrap_content" />
|
||||
|
||||
<SwitchPreference
|
||||
android:id="@+id/mousepad_mouse_buttons_enabled_pref"
|
||||
|
@@ -8,29 +8,28 @@ package org.kde.kdeconnect.Backends;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
|
||||
public abstract class BaseLink {
|
||||
|
||||
protected final Context context;
|
||||
|
||||
public interface PacketReceiver {
|
||||
void onPacketReceived(NetworkPacket np);
|
||||
void onPacketReceived(@NonNull NetworkPacket np);
|
||||
}
|
||||
|
||||
protected final Context context;
|
||||
private final BaseLinkProvider linkProvider;
|
||||
private final String deviceId;
|
||||
private final ArrayList<PacketReceiver> receivers = new ArrayList<>();
|
||||
protected PrivateKey privateKey;
|
||||
|
||||
protected BaseLink(Context context, String deviceId, BaseLinkProvider linkProvider) {
|
||||
protected BaseLink(@NonNull Context context, @NonNull String deviceId, @NonNull BaseLinkProvider linkProvider) {
|
||||
this.context = context;
|
||||
this.linkProvider = linkProvider;
|
||||
this.deviceId = deviceId;
|
||||
@@ -38,34 +37,25 @@ public abstract class BaseLink {
|
||||
|
||||
/* To be implemented by each link for pairing handlers */
|
||||
public abstract String getName();
|
||||
public abstract BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback);
|
||||
public abstract BasePairingHandler getPairingHandler(@NonNull Device device, @NonNull BasePairingHandler.PairingHandlerCallback callback);
|
||||
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public void setPrivateKey(PrivateKey key) {
|
||||
privateKey = key;
|
||||
}
|
||||
|
||||
public BaseLinkProvider getLinkProvider() {
|
||||
return linkProvider;
|
||||
}
|
||||
|
||||
//The daemon will periodically destroy unpaired links if this returns false
|
||||
public boolean linkShouldBeKeptAlive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addPacketReceiver(PacketReceiver pr) {
|
||||
public void addPacketReceiver(@NonNull PacketReceiver pr) {
|
||||
receivers.add(pr);
|
||||
}
|
||||
public void removePacketReceiver(PacketReceiver pr) {
|
||||
public void removePacketReceiver(@NonNull PacketReceiver pr) {
|
||||
receivers.remove(pr);
|
||||
}
|
||||
|
||||
//Should be called from a background thread listening for packets
|
||||
protected void packetReceived(NetworkPacket np) {
|
||||
protected void packetReceived(@NonNull NetworkPacket np) {
|
||||
for(PacketReceiver pr : receivers) {
|
||||
pr.onPacketReceived(np);
|
||||
}
|
||||
@@ -75,7 +65,7 @@ public abstract class BaseLink {
|
||||
linkProvider.connectionLost(this);
|
||||
}
|
||||
|
||||
//TO OVERRIDE, should be sync
|
||||
//TO OVERRIDE, should be sync. If sendPayloadFromSameThread is false, it should only block to send the packet but start a separate thread to send the payload.
|
||||
@WorkerThread
|
||||
public abstract boolean sendPacket(NetworkPacket np, Device.SendPacketStatusCallback callback);
|
||||
public abstract boolean sendPacket(@NonNull NetworkPacket np, @NonNull Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) throws IOException;
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.json.JSONException;
|
||||
@@ -137,7 +138,8 @@ public class BluetoothLink extends BaseLink {
|
||||
|
||||
@WorkerThread
|
||||
@Override
|
||||
public boolean sendPacket(NetworkPacket np, final Device.SendPacketStatusCallback callback) {
|
||||
public boolean sendPacket(@NonNull NetworkPacket np, @NonNull Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) throws IOException {
|
||||
// sendPayloadFromSameThread is ignored, we always send from the same thread!
|
||||
|
||||
/*if (!isConnected()) {
|
||||
Log.e("BluetoothLink", "sendPacketEncrypted failed: not connected");
|
||||
@@ -168,7 +170,7 @@ public class BluetoothLink extends BaseLink {
|
||||
progress += bytesRead;
|
||||
payloadStream.write(buffer, 0, bytesRead);
|
||||
if (np.getPayloadSize() > 0) {
|
||||
callback.onProgressChanged((int) (100 * progress / np.getPayloadSize()));
|
||||
callback.onPayloadProgressChanged((int) (100 * progress / np.getPayloadSize()));
|
||||
}
|
||||
}
|
||||
payloadStream.flush();
|
||||
@@ -186,11 +188,6 @@ public class BluetoothLink extends BaseLink {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean linkShouldBeKeptAlive() {
|
||||
return receivingThread.isAlive();
|
||||
}
|
||||
|
||||
/*
|
||||
public boolean isConnected() {
|
||||
return socket.isConnected();
|
||||
|
@@ -378,7 +378,7 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
|
||||
public void onFailure(Throwable e) {
|
||||
|
||||
}
|
||||
});
|
||||
}, true);
|
||||
} catch (Exception e) {
|
||||
Log.e("BTLinkProvider/Client", "Connection lost/disconnected on " + device.getAddress(), e);
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ package org.kde.kdeconnect.Backends.LanBackend;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.json.JSONObject;
|
||||
@@ -131,7 +132,7 @@ public class LanLink extends BaseLink {
|
||||
//Blocking, do not call from main thread
|
||||
@WorkerThread
|
||||
@Override
|
||||
public boolean sendPacket(NetworkPacket np, final Device.SendPacketStatusCallback callback) {
|
||||
public boolean sendPacket(@NonNull NetworkPacket np, @NonNull final Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {
|
||||
if (socket == null) {
|
||||
Log.e("KDE/sendPacket", "Not yet connected");
|
||||
callback.onFailure(new NotYetConnectedException());
|
||||
@@ -165,51 +166,17 @@ public class LanLink extends BaseLink {
|
||||
|
||||
//Send payload
|
||||
if (server != null) {
|
||||
Socket payloadSocket = null;
|
||||
OutputStream outputStream = null;
|
||||
InputStream inputStream;
|
||||
try {
|
||||
//Wait a maximum of 10 seconds for the other end to establish a connection with our socket, close it afterwards
|
||||
server.setSoTimeout(10*1000);
|
||||
|
||||
payloadSocket = server.accept();
|
||||
|
||||
//Convert to SSL if needed
|
||||
payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false);
|
||||
|
||||
outputStream = payloadSocket.getOutputStream();
|
||||
inputStream = np.getPayload().getInputStream();
|
||||
|
||||
Log.i("KDE/LanLink", "Beginning to send payload");
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
long size = np.getPayloadSize();
|
||||
long progress = 0;
|
||||
long timeSinceLastUpdate = -1;
|
||||
while (!np.isCanceled() && (bytesRead = inputStream.read(buffer)) != -1) {
|
||||
//Log.e("ok",""+bytesRead);
|
||||
progress += bytesRead;
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
if (size > 0) {
|
||||
if (timeSinceLastUpdate + 500 < System.currentTimeMillis()) { //Report progress every half a second
|
||||
long percent = ((100 * progress) / size);
|
||||
callback.onProgressChanged((int) percent);
|
||||
timeSinceLastUpdate = System.currentTimeMillis();
|
||||
}
|
||||
if (sendPayloadFromSameThread) {
|
||||
sendPayload(np, callback, server);
|
||||
} else {
|
||||
ThreadHelper.execute(() -> {
|
||||
try {
|
||||
sendPayload(np, callback, server);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e("LanLink/sendPacket", "Async sendPayload failed for packet of type " + np.getType() + ". The Plugin was NOT notified.");
|
||||
}
|
||||
}
|
||||
outputStream.flush();
|
||||
Log.i("KDE/LanLink", "Finished sending payload ("+progress+" bytes written)");
|
||||
} catch(SSLHandshakeException e) {
|
||||
// The exception can be due to several causes. "Connection closed by peer" seems to be a common one.
|
||||
// If we could distinguish different cases we could react differently for some of them, but I haven't found how.
|
||||
Log.e("sendPacket","Payload SSLSocket failed");
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try { server.close(); } catch (Exception ignored) { }
|
||||
try { payloadSocket.close(); } catch (Exception ignored) { }
|
||||
np.getPayload().close();
|
||||
try { outputStream.close(); } catch (Exception ignored) { }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +197,59 @@ public class LanLink extends BaseLink {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPayload(NetworkPacket np, Device.SendPacketStatusCallback callback, ServerSocket server) throws IOException {
|
||||
Socket payloadSocket = null;
|
||||
OutputStream outputStream = null;
|
||||
InputStream inputStream;
|
||||
try {
|
||||
if (!np.isCanceled()) {
|
||||
//Wait a maximum of 10 seconds for the other end to establish a connection with our socket, close it afterwards
|
||||
server.setSoTimeout(10 * 1000);
|
||||
|
||||
payloadSocket = server.accept();
|
||||
|
||||
//Convert to SSL if needed
|
||||
payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false);
|
||||
|
||||
outputStream = payloadSocket.getOutputStream();
|
||||
inputStream = np.getPayload().getInputStream();
|
||||
|
||||
Log.i("KDE/LanLink", "Beginning to send payload for " + np.getType());
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
long size = np.getPayloadSize();
|
||||
long progress = 0;
|
||||
long timeSinceLastUpdate = -1;
|
||||
while (!np.isCanceled() && (bytesRead = inputStream.read(buffer)) != -1) {
|
||||
//Log.e("ok",""+bytesRead);
|
||||
progress += bytesRead;
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
if (size > 0) {
|
||||
if (timeSinceLastUpdate + 500 < System.currentTimeMillis()) { //Report progress every half a second
|
||||
long percent = ((100 * progress) / size);
|
||||
callback.onPayloadProgressChanged((int) percent);
|
||||
timeSinceLastUpdate = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
}
|
||||
outputStream.flush();
|
||||
Log.i("KDE/LanLink", "Finished sending payload (" + progress + " bytes written)");
|
||||
}
|
||||
} catch(SocketTimeoutException e) {
|
||||
Log.e("LanLink", "Socket for payload in packet " + np.getType() + " timed out. The other end didn't fetch the payload.");
|
||||
} catch(SSLHandshakeException e) {
|
||||
// The exception can be due to several causes. "Connection closed by peer" seems to be a common one.
|
||||
// If we could distinguish different cases we could react differently for some of them, but I haven't found how.
|
||||
Log.e("sendPacket","Payload SSLSocket failed");
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try { server.close(); } catch (Exception ignored) { }
|
||||
try { payloadSocket.close(); } catch (Exception ignored) { }
|
||||
np.getPayload().close();
|
||||
try { outputStream.close(); } catch (Exception ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
private void receivedNetworkPacket(NetworkPacket np) {
|
||||
|
||||
if (np.hasPayloadTransferInfo()) {
|
||||
@@ -250,14 +270,4 @@ public class LanLink extends BaseLink {
|
||||
packetReceived(np);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean linkShouldBeKeptAlive() {
|
||||
|
||||
return true; //FIXME: Current implementation is broken, so for now we will keep links always established
|
||||
|
||||
//We keep the remotely initiated connections, since the remotes require them if they want to request
|
||||
//pairing to us, or connections that are already paired.
|
||||
//return (connectionSource == ConnectionStarted.Remotely);
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -14,12 +14,12 @@ import android.util.Log;
|
||||
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Device;
|
||||
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;
|
||||
|
||||
@@ -196,13 +196,13 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
|
||||
if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) {
|
||||
//Device paired with and old version, we can't use it as we lack the certificate
|
||||
BackgroundService.RunCommand(context, service -> {
|
||||
Device device = service.getDevice(deviceId);
|
||||
if (device == null) return;
|
||||
device.unpair();
|
||||
//Retry as unpaired
|
||||
identityPacketReceived(identityPacket, socket, connectionStarted);
|
||||
});
|
||||
Device device = KdeConnect.getInstance().getDevice(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
device.unpair();
|
||||
//Retry as unpaired
|
||||
identityPacketReceived(identityPacket, socket, connectionStarted);
|
||||
}
|
||||
|
||||
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted);
|
||||
@@ -217,11 +217,11 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
addLink(identityPacket, sslsocket, connectionStarted);
|
||||
} catch (Exception e) {
|
||||
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e);
|
||||
BackgroundService.RunCommand(context, service -> {
|
||||
Device device = service.getDevice(deviceId);
|
||||
if (device == null) return;
|
||||
device.unpair();
|
||||
});
|
||||
Device device = KdeConnect.getInstance().getDevice(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
device.unpair();
|
||||
}
|
||||
});
|
||||
//Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection
|
||||
@@ -369,7 +369,7 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
|
||||
NetworkPacket identity = NetworkPacket.createIdentityPacket(context);
|
||||
if (tcpServer == null || !tcpServer.isBound()) {
|
||||
throw new RuntimeException("Wont't broadcast UDP packet if TCP socket is not ready");
|
||||
throw new IllegalStateException("Wont't broadcast UDP packet if TCP socket is not ready");
|
||||
}
|
||||
int port = tcpServer.getLocalPort();
|
||||
identity.set("tcpPort", port);
|
||||
@@ -420,7 +420,11 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
|
||||
@Override
|
||||
public void onNetworkChange() {
|
||||
broadcastUdpPacket();
|
||||
try {
|
||||
broadcastUdpPacket();
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -8,6 +8,7 @@ package org.kde.kdeconnect.Backends.LoopbackBackend;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
@@ -34,12 +35,12 @@ public class LoopbackLink extends BaseLink {
|
||||
|
||||
@WorkerThread
|
||||
@Override
|
||||
public boolean sendPacket(NetworkPacket in, Device.SendPacketStatusCallback callback) {
|
||||
public boolean sendPacket(@NonNull NetworkPacket in, @NonNull Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {
|
||||
packetReceived(in);
|
||||
if (in.hasPayload()) {
|
||||
callback.onProgressChanged(0);
|
||||
callback.onPayloadProgressChanged(0);
|
||||
in.setPayload(in.getPayload());
|
||||
callback.onProgressChanged(100);
|
||||
callback.onPayloadProgressChanged(100);
|
||||
}
|
||||
callback.onSuccess();
|
||||
return true;
|
||||
|
@@ -14,34 +14,26 @@ import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkRequest;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Helpers.NotificationHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper;
|
||||
import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardFloatingActivity;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandActivity;
|
||||
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin;
|
||||
import org.kde.kdeconnect.Plugins.SharePlugin.SendFileActivity;
|
||||
@@ -49,92 +41,34 @@ import org.kde.kdeconnect.UserInterface.MainActivity;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
//import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider;
|
||||
|
||||
/*
|
||||
* This class (still) does 3 things:
|
||||
* - Keeps the app running by creating a foreground notification.
|
||||
* - Holds references to the active LinkProviders, but doesn't handle the DeviceLink those create (the KdeConnect class does that).
|
||||
* - Listens for network connectivity changes and tells the LinkProviders to re-check for devices.
|
||||
* It can be started by the KdeConnectBroadcastReceiver on some events or when the MainActivity is launched.
|
||||
*/
|
||||
public class BackgroundService extends Service {
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
||||
|
||||
private static BackgroundService instance;
|
||||
|
||||
public interface DeviceListChangedCallback {
|
||||
void onDeviceListChanged(boolean isConnectedToNonCellularNetwork);
|
||||
}
|
||||
|
||||
public interface PluginCallback<T extends Plugin> {
|
||||
void run(T plugin);
|
||||
}
|
||||
|
||||
private final ConcurrentHashMap<String, DeviceListChangedCallback> deviceListChangedCallbacks = new ConcurrentHashMap<>();
|
||||
private KdeConnect applicationInstance;
|
||||
|
||||
private final ArrayList<BaseLinkProvider> linkProviders = new ArrayList<>();
|
||||
|
||||
private final ConcurrentHashMap<String, Device> devices = new ConcurrentHashMap<>();
|
||||
|
||||
private final HashSet<Object> discoveryModeAcquisitions = new HashSet<>();
|
||||
|
||||
public static BackgroundService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
boolean isConnectedToNonCellularNetwork; // True when connected over wifi/usb/bluetooth/(anything other than cellular)
|
||||
|
||||
private boolean acquireDiscoveryMode(Object key) {
|
||||
boolean wasEmpty = discoveryModeAcquisitions.isEmpty();
|
||||
discoveryModeAcquisitions.add(key);
|
||||
if (wasEmpty) {
|
||||
onNetworkChange();
|
||||
}
|
||||
//Log.e("acquireDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]");
|
||||
return wasEmpty;
|
||||
// This indicates when connected over wifi/usb/bluetooth/(anything other than cellular)
|
||||
private final MutableLiveData<Boolean> connectedToNonCellularNetwork = new MutableLiveData<>();
|
||||
public LiveData<Boolean> isConnectedToNonCellularNetwork() {
|
||||
return connectedToNonCellularNetwork;
|
||||
}
|
||||
|
||||
private void releaseDiscoveryMode(Object key) {
|
||||
boolean removed = discoveryModeAcquisitions.remove(key);
|
||||
//Log.e("releaseDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]");
|
||||
if (removed && discoveryModeAcquisitions.isEmpty()) {
|
||||
cleanDevices();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInDiscoveryMode() {
|
||||
//return !discoveryModeAcquisitions.isEmpty();
|
||||
return true; // Keep it always on for now
|
||||
}
|
||||
|
||||
private final Device.PairingCallback devicePairingCallback = new Device.PairingCallback() {
|
||||
@Override
|
||||
public void incomingRequest() {
|
||||
onDeviceListChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pairingSuccessful() {
|
||||
onDeviceListChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pairingFailed(String error) {
|
||||
onDeviceListChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unpaired() {
|
||||
onDeviceListChanged();
|
||||
}
|
||||
};
|
||||
|
||||
public void onDeviceListChanged() {
|
||||
for (DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) {
|
||||
callback.onDeviceListChanged(isConnectedToNonCellularNetwork);
|
||||
}
|
||||
|
||||
public void updateForegroundNotification() {
|
||||
if (NotificationHelper.isPersistentNotificationEnabled(this)) {
|
||||
//Update the foreground notification with the currently connected device list
|
||||
NotificationManager nm = ContextCompat.getSystemService(this, NotificationManager.class);
|
||||
@@ -142,97 +76,14 @@ public class BackgroundService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private void loadRememberedDevicesFromSettings() {
|
||||
//Log.e("BackgroundService", "Loading remembered trusted devices");
|
||||
SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
|
||||
Set<String> trustedDevices = preferences.getAll().keySet();
|
||||
for (String deviceId : trustedDevices) {
|
||||
//Log.e("BackgroundService", "Loading device "+deviceId);
|
||||
if (preferences.getBoolean(deviceId, false)) {
|
||||
Device device = new Device(this, deviceId);
|
||||
devices.put(deviceId, device);
|
||||
device.addPairingCallback(devicePairingCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void registerLinkProviders() {
|
||||
linkProviders.add(new LanLinkProvider(this));
|
||||
// linkProviders.add(new LoopbackLinkProvider(this));
|
||||
// linkProviders.add(new BluetoothLinkProvider(this));
|
||||
}
|
||||
|
||||
public ArrayList<BaseLinkProvider> getLinkProviders() {
|
||||
return linkProviders;
|
||||
}
|
||||
|
||||
public Device getDevice(String id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return devices.get(id);
|
||||
}
|
||||
|
||||
private void cleanDevices() {
|
||||
ThreadHelper.execute(() -> {
|
||||
for (Device d : devices.values()) {
|
||||
if (!d.isPaired() && !d.isPairRequested() && !d.isPairRequestedByPeer() && !d.deviceShouldBeKeptAlive()) {
|
||||
d.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private final BaseLinkProvider.ConnectionReceiver deviceListener = new BaseLinkProvider.ConnectionReceiver() {
|
||||
@Override
|
||||
public void onConnectionReceived(final NetworkPacket identityPacket, final BaseLink link) {
|
||||
|
||||
String deviceId = identityPacket.getString("deviceId");
|
||||
|
||||
Device device = devices.get(deviceId);
|
||||
|
||||
if (device != null) {
|
||||
Log.i("KDE/BackgroundService", "addLink, known device: " + deviceId);
|
||||
device.addLink(identityPacket, link);
|
||||
} else {
|
||||
Log.i("KDE/BackgroundService", "addLink,unknown device: " + deviceId);
|
||||
device = new Device(BackgroundService.this, identityPacket, link);
|
||||
if (device.isPaired() || device.isPairRequested() || device.isPairRequestedByPeer()
|
||||
|| link.linkShouldBeKeptAlive()
|
||||
|| isInDiscoveryMode()) {
|
||||
devices.put(deviceId, device);
|
||||
device.addPairingCallback(devicePairingCallback);
|
||||
} else {
|
||||
device.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
onDeviceListChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionLost(BaseLink link) {
|
||||
Device d = devices.get(link.getDeviceId());
|
||||
Log.i("KDE/onConnectionLost", "removeLink, deviceId: " + link.getDeviceId());
|
||||
if (d != null) {
|
||||
d.removeLink(link);
|
||||
if (!d.isReachable() && !d.isPaired()) {
|
||||
//Log.e("onConnectionLost","Removing connection device because it was not paired");
|
||||
devices.remove(link.getDeviceId());
|
||||
d.removePairingCallback(devicePairingCallback);
|
||||
}
|
||||
} else {
|
||||
//Log.d("KDE/onConnectionLost","Removing connection to unknown device");
|
||||
}
|
||||
onDeviceListChanged();
|
||||
}
|
||||
};
|
||||
|
||||
public ConcurrentHashMap<String, Device> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
public void onNetworkChange() {
|
||||
Log.d("KDE/BackgroundService", "onNetworkChange");
|
||||
for (BaseLinkProvider a : linkProviders) {
|
||||
a.onNetworkChange();
|
||||
}
|
||||
@@ -250,22 +101,14 @@ public class BackgroundService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
public void addDeviceListChangedCallback(String key, DeviceListChangedCallback callback) {
|
||||
deviceListChangedCallbacks.put(key, callback);
|
||||
}
|
||||
|
||||
public void removeDeviceListChangedCallback(String key) {
|
||||
deviceListChangedCallbacks.remove(key);
|
||||
}
|
||||
|
||||
//This will called only once, even if we launch the service intent several times
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Log.d("KdeConnect/BgService", "onCreate");
|
||||
instance = this;
|
||||
|
||||
DeviceHelper.initializeDeviceId(this);
|
||||
KdeConnect.getInstance().addDeviceListChangedCallback("BackgroundService", this::updateForegroundNotification);
|
||||
|
||||
// Register screen on listener
|
||||
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
|
||||
@@ -281,29 +124,19 @@ public class BackgroundService extends Service {
|
||||
cm.registerNetworkCallback(networkRequestBuilder.build(), new ConnectivityManager.NetworkCallback() {
|
||||
@Override
|
||||
public void onAvailable(Network network) {
|
||||
isConnectedToNonCellularNetwork = true;
|
||||
onDeviceListChanged();
|
||||
connectedToNonCellularNetwork.postValue(true);
|
||||
onNetworkChange();
|
||||
}
|
||||
@Override
|
||||
public void onLost(Network network) {
|
||||
isConnectedToNonCellularNetwork = false;
|
||||
onDeviceListChanged();
|
||||
connectedToNonCellularNetwork.postValue(false);
|
||||
}
|
||||
});
|
||||
|
||||
Log.i("KDE/BackgroundService", "Service not started yet, initializing...");
|
||||
applicationInstance = KdeConnect.getInstance();
|
||||
|
||||
PluginFactory.initPluginInfo(getBaseContext());
|
||||
initializeSecurityParameters();
|
||||
NotificationHelper.initializeChannels(this);
|
||||
loadRememberedDevicesFromSettings();
|
||||
migratePluginSettings();
|
||||
registerLinkProviders();
|
||||
|
||||
//Link Providers need to be already registered
|
||||
addConnectionListener(deviceListener);
|
||||
|
||||
addConnectionListener(applicationInstance.getConnectionListener()); // Link Providers need to be already registered
|
||||
for (BaseLinkProvider a : linkProviders) {
|
||||
a.onStart();
|
||||
}
|
||||
@@ -325,36 +158,11 @@ public class BackgroundService extends Service {
|
||||
return networkRequestBuilder;
|
||||
}
|
||||
|
||||
private void migratePluginSettings() {
|
||||
SharedPreferences globalPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
for (String pluginKey : PluginFactory.getAvailablePlugins()) {
|
||||
if (PluginFactory.getPluginInfo(pluginKey).supportsDeviceSpecificSettings()) {
|
||||
Iterator<Device> it = devices.values().iterator();
|
||||
|
||||
while (it.hasNext()) {
|
||||
Device device = it.next();
|
||||
Plugin plugin = PluginFactory.instantiatePluginForDevice(getBaseContext(), pluginKey, device);
|
||||
|
||||
if (plugin == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
plugin.copyGlobalToDeviceSpecificSettings(globalPrefs);
|
||||
if (!it.hasNext()) {
|
||||
plugin.removeSettings(globalPrefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void changePersistentNotificationVisibility(boolean visible) {
|
||||
NotificationManager nm = ContextCompat.getSystemService(this, NotificationManager.class);
|
||||
if (visible) {
|
||||
nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
|
||||
updateForegroundNotification();
|
||||
} else {
|
||||
stopForeground(true);
|
||||
Stop();
|
||||
Start(this);
|
||||
}
|
||||
}
|
||||
@@ -365,7 +173,7 @@ public class BackgroundService extends Service {
|
||||
|
||||
ArrayList<String> connectedDevices = new ArrayList<>();
|
||||
ArrayList<String> connectedDeviceIds = new ArrayList<>();
|
||||
for (Device device : getDevices().values()) {
|
||||
for (Device device : applicationInstance.getDevices().values()) {
|
||||
if (device.isReachable() && device.isPaired()) {
|
||||
connectedDeviceIds.add(device.getDeviceId());
|
||||
connectedDevices.add(device.getName());
|
||||
@@ -409,7 +217,7 @@ public class BackgroundService extends Service {
|
||||
|
||||
if (connectedDeviceIds.size() == 1) {
|
||||
String deviceId = connectedDeviceIds.get(0);
|
||||
Device device = getDevice(deviceId);
|
||||
Device device = KdeConnect.getInstance().getDevice(deviceId);
|
||||
if (device != null) {
|
||||
// Adding two action buttons only when there is a single device connected.
|
||||
// Setting up Send File Intent.
|
||||
@@ -432,49 +240,24 @@ public class BackgroundService extends Service {
|
||||
return notification.build();
|
||||
}
|
||||
|
||||
private void initializeSecurityParameters() {
|
||||
RsaHelper.initialiseRsaKeys(this);
|
||||
SslHelper.initialiseCertificate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
stopForeground(true);
|
||||
Log.d("KdeConnect/BgService", "onDestroy");
|
||||
for (BaseLinkProvider a : linkProviders) {
|
||||
a.onStop();
|
||||
}
|
||||
KdeConnect.getInstance().removeDeviceListChangedCallback("BackgroundService");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return new Binder();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
//To use the service from the gui
|
||||
|
||||
public interface InstanceCallback {
|
||||
void onServiceStart(BackgroundService service);
|
||||
}
|
||||
|
||||
private final static ArrayList<InstanceCallback> callbacks = new ArrayList<>();
|
||||
|
||||
private final static Lock mutex = new ReentrantLock(true);
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
//This will be called for each intent launch, even if the service is already started and it is reused
|
||||
mutex.lock();
|
||||
try {
|
||||
for (InstanceCallback c : callbacks) {
|
||||
c.onServiceStart(this);
|
||||
}
|
||||
callbacks.clear();
|
||||
} finally {
|
||||
mutex.unlock();
|
||||
}
|
||||
|
||||
Log.d("KDE/BackgroundService", "onStartCommand");
|
||||
if (NotificationHelper.isPersistentNotificationEnabled(this)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
|
||||
@@ -482,43 +265,27 @@ public class BackgroundService extends Service {
|
||||
startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
|
||||
}
|
||||
}
|
||||
if (intent != null && intent.getBooleanExtra("refresh", false)) {
|
||||
onNetworkChange();
|
||||
}
|
||||
return Service.START_STICKY;
|
||||
}
|
||||
|
||||
private static void Start(Context c) {
|
||||
RunCommand(c, null);
|
||||
public static void Start(Context context) {
|
||||
Log.d("KDE/BackgroundService", "Start");
|
||||
Intent intent = new Intent(context, BackgroundService.class);
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
public static void RunCommand(final Context c, final InstanceCallback callback) {
|
||||
ThreadHelper.execute(() -> {
|
||||
if (callback != null) {
|
||||
mutex.lock();
|
||||
try {
|
||||
callbacks.add(callback);
|
||||
} finally {
|
||||
mutex.unlock();
|
||||
}
|
||||
}
|
||||
ContextCompat.startForegroundService(c, new Intent(c, BackgroundService.class));
|
||||
});
|
||||
public static void ForceRefreshConnections(Context context) {
|
||||
Log.d("KDE/BackgroundService", "ForceRefreshConnections");
|
||||
Intent intent = new Intent(context, BackgroundService.class);
|
||||
intent.putExtra("refresh", true);
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
public static <T extends Plugin> void RunWithPlugin(final Context c, final String deviceId, final Class<T> pluginClass, final PluginCallback<T> cb) {
|
||||
RunCommand(c, service -> {
|
||||
Device device = service.getDevice(deviceId);
|
||||
|
||||
if (device == null) {
|
||||
Log.e("BackgroundService", "Device " + deviceId + " not found");
|
||||
return;
|
||||
}
|
||||
|
||||
final T plugin = device.getPlugin(pluginClass);
|
||||
|
||||
if (plugin == null) {
|
||||
Log.e("BackgroundService", "Device " + device.getName() + " does not have plugin " + pluginClass.getName());
|
||||
return;
|
||||
}
|
||||
cb.run(plugin);
|
||||
});
|
||||
public void Stop() {
|
||||
stopForeground(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -38,10 +37,8 @@ import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.UserInterface.MainActivity;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.io.IOException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
@@ -92,7 +89,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
}
|
||||
|
||||
public interface PluginsChangedListener {
|
||||
void onPluginsChanged(Device device);
|
||||
void onPluginsChanged(@NonNull Device device);
|
||||
}
|
||||
|
||||
public enum PairStatus {
|
||||
@@ -364,9 +361,6 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
//
|
||||
// Notification related methods used during pairing
|
||||
//
|
||||
public int getNotificationId() {
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
public void displayPairingNotification() {
|
||||
|
||||
@@ -383,11 +377,9 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
Intent rejectIntent = new Intent(getContext(), MainActivity.class);
|
||||
|
||||
acceptIntent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId());
|
||||
//acceptIntent.putExtra("notificationId", notificationId);
|
||||
acceptIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_ACCEPTED);
|
||||
|
||||
rejectIntent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId());
|
||||
//rejectIntent.putExtra("notificationId", notificationId);
|
||||
rejectIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_REJECTED);
|
||||
|
||||
PendingIntent acceptedPendingIntent = PendingIntent.getActivity(getContext(), 2, acceptIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE);
|
||||
@@ -404,6 +396,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
.setContentText(res.getString(R.string.pairing_verification_code, verificationKeyShort))
|
||||
.setTicker(res.getString(R.string.pair_requested))
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentIntent(pendingIntent)
|
||||
.addAction(R.drawable.ic_accept_pairing_24dp, res.getString(R.string.pairing_accept), acceptedPendingIntent)
|
||||
.addAction(R.drawable.ic_reject_pairing_24dp, res.getString(R.string.pairing_reject), rejectedPendingIntent)
|
||||
.setAutoCancel(true)
|
||||
@@ -461,15 +454,6 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
byte[] privateKeyBytes = Base64.decode(globalSettings.getString("privateKey", ""), 0);
|
||||
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
|
||||
link.setPrivateKey(privateKey);
|
||||
} catch (Exception e) {
|
||||
Log.e("KDE/Device", "Exception reading our own private key", e); //Should not happen
|
||||
}
|
||||
|
||||
Log.i("KDE/Device", "addLink " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size());
|
||||
|
||||
if (!pairingHandlers.containsKey(link.getName())) {
|
||||
@@ -609,7 +593,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
|
||||
public abstract void onFailure(Throwable e);
|
||||
|
||||
public void onProgressChanged(int percent) {
|
||||
public void onPayloadProgressChanged(int percent) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,22 +609,22 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
};
|
||||
|
||||
@AnyThread
|
||||
public void sendPacket(NetworkPacket np) {
|
||||
public void sendPacket(@NonNull NetworkPacket np) {
|
||||
sendPacket(np, -1, defaultCallback);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public void sendPacket(NetworkPacket np, int replaceID) {
|
||||
public void sendPacket(@NonNull NetworkPacket np, int replaceID) {
|
||||
sendPacket(np, replaceID, defaultCallback);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public boolean sendPacketBlocking(NetworkPacket np) {
|
||||
public boolean sendPacketBlocking(@NonNull NetworkPacket np) {
|
||||
return sendPacketBlocking(np, defaultCallback);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public void sendPacket(final NetworkPacket np, final SendPacketStatusCallback callback) {
|
||||
public void sendPacket(@NonNull final NetworkPacket np, @NonNull final SendPacketStatusCallback callback) {
|
||||
sendPacket(np, -1, callback);
|
||||
}
|
||||
|
||||
@@ -651,7 +635,7 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
* @param callback A callback for success/failure
|
||||
*/
|
||||
@AnyThread
|
||||
public void sendPacket(final NetworkPacket np, int replaceID, final SendPacketStatusCallback callback) {
|
||||
public void sendPacket(@NonNull final NetworkPacket np, int replaceID, @NonNull final SendPacketStatusCallback callback) {
|
||||
if (packetQueue == null) {
|
||||
callback.onFailure(new Exception("Device disconnected!"));
|
||||
} else {
|
||||
@@ -673,16 +657,24 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public boolean sendPacketBlocking(@NonNull final NetworkPacket np, @NonNull final SendPacketStatusCallback callback) {
|
||||
return sendPacketBlocking(np, callback, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send {@code np} over one of this device's connected {@link #links}.
|
||||
*
|
||||
* @param np the packet to send
|
||||
* @param callback a callback that can receive realtime updates
|
||||
* @param np the packet to send
|
||||
* @param callback a callback that can receive realtime updates
|
||||
* @param sendPayloadFromSameThread when set to true and np contains a Payload, this function
|
||||
* won't return until the Payload has been received by the
|
||||
* other end, or times out after 10 seconds
|
||||
* @return true if the packet was sent ok, false otherwise
|
||||
* @see BaseLink#sendPacket(NetworkPacket, SendPacketStatusCallback)
|
||||
*/
|
||||
@WorkerThread
|
||||
public boolean sendPacketBlocking(final NetworkPacket np, final SendPacketStatusCallback callback) {
|
||||
public boolean sendPacketBlocking(@NonNull final NetworkPacket np, @NonNull final SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {
|
||||
|
||||
/*
|
||||
if (!m_outgoingCapabilities.contains(np.getType()) && !NetworkPacket.protocolPacketTypes.contains(np.getType())) {
|
||||
@@ -692,12 +684,14 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
*/
|
||||
|
||||
boolean success = false;
|
||||
//Make a copy to avoid concurrent modification exception if the original list changes
|
||||
for (final BaseLink link : links) {
|
||||
if (link == null)
|
||||
continue; //Since we made a copy, maybe somebody destroyed the link in the meanwhile
|
||||
success = link.sendPacket(np, callback);
|
||||
if (success) break; //If the link didn't call sendSuccess(), try the next one
|
||||
if (link == null) continue;
|
||||
try {
|
||||
success = link.sendPacket(np, callback, sendPayloadFromSameThread);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (success) break;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
@@ -880,22 +874,6 @@ public class Device implements BaseLink.PacketReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean deviceShouldBeKeptAlive() {
|
||||
|
||||
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
|
||||
if (preferences.contains(getDeviceId())) {
|
||||
//Log.e("DeviceShouldBeKeptAlive", "because it's a paired device");
|
||||
return true; //Already paired
|
||||
}
|
||||
|
||||
for (BaseLink l : links) {
|
||||
if (l.linkShouldBeKeptAlive()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<String> getSupportedPlugins() {
|
||||
return supportedPlugins;
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package org.kde.kdeconnect;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
@@ -101,7 +103,10 @@ class DevicePacketQueue {
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
if (exit) break;
|
||||
if (exit) {
|
||||
Log.i("DevicePacketQueue", "Terminating sending loop");
|
||||
break;
|
||||
}
|
||||
|
||||
item = items.removeFirst();
|
||||
}
|
||||
|
@@ -15,7 +15,6 @@ import android.os.Build;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.kde.kdeconnect.MyApplication;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
public class IntentHelper {
|
||||
@@ -27,8 +26,8 @@ public class IntentHelper {
|
||||
* @param intent the Intent to be started
|
||||
* @param title a title which is shown in the notification on Android 10+
|
||||
*/
|
||||
public static void startActivityFromBackground(Context context, Intent intent, String title) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !MyApplication.isInForeground()) {
|
||||
public static void startActivityFromBackgroundOrCreateNotification(Context context, Intent intent, String title) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !LifecycleHelper.isInForeground()) {
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE);
|
||||
Notification notification = new NotificationCompat
|
||||
.Builder(context, NotificationHelper.Channels.HIGHPRIORITY)
|
||||
|
@@ -1,15 +1,12 @@
|
||||
package org.kde.kdeconnect;
|
||||
|
||||
import android.app.Application;
|
||||
package org.kde.kdeconnect.Helpers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
public class LifecycleHelper {
|
||||
|
||||
public class MyApplication extends Application {
|
||||
private static class LifecycleObserver implements DefaultLifecycleObserver {
|
||||
private boolean inForeground = false;
|
||||
|
||||
@@ -28,16 +25,13 @@ public class MyApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private static final LifecycleObserver foregroundTracker = new LifecycleObserver();
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
ThemeUtil.setUserPreferredTheme(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(foregroundTracker);
|
||||
}
|
||||
private final static LifecycleObserver foregroundTracker = new LifecycleObserver();
|
||||
|
||||
public static boolean isInForeground() {
|
||||
return foregroundTracker.isInForeground();
|
||||
}
|
||||
|
||||
public static void initializeObserver() {
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(foregroundTracker);
|
||||
}
|
||||
}
|
@@ -209,7 +209,7 @@ public class SslHelper {
|
||||
trustManagerFactory.init(keyStore);
|
||||
|
||||
// Setup custom trust manager if device not trusted
|
||||
SSLContext tlsContext = SSLContext.getInstance("TLSv1"); //Newer TLS versions are only supported on API 16+
|
||||
SSLContext tlsContext = SSLContext.getInstance("TLSv1.2"); // Use TLS up to 1.2, since 1.3 seems to cause issues in some (older?) devices
|
||||
if (isDeviceTrusted) {
|
||||
tlsContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), RandomHelper.secureRandom);
|
||||
} else {
|
||||
|
174
src/org/kde/kdeconnect/KdeConnect.java
Normal file
@@ -0,0 +1,174 @@
|
||||
package org.kde.kdeconnect;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Helpers.LifecycleHelper;
|
||||
import org.kde.kdeconnect.Helpers.NotificationHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/*
|
||||
* This class holds all the active devices and makes them accessible from every other class.
|
||||
* It also takes care of initializing all classes that need so when the app boots.
|
||||
* It provides a ConnectionReceiver that the BackgroundService uses to ping this class every time a new DeviceLink is created.
|
||||
*/
|
||||
public class KdeConnect extends Application {
|
||||
|
||||
public interface DeviceListChangedCallback {
|
||||
void onDeviceListChanged();
|
||||
}
|
||||
|
||||
private static KdeConnect instance = null;
|
||||
|
||||
private final ConcurrentHashMap<String, Device> devices = new ConcurrentHashMap<>();
|
||||
|
||||
private final ConcurrentHashMap<String, DeviceListChangedCallback> deviceListChangedCallbacks = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
Log.d("KdeConnect/Application", "onCreate");
|
||||
ThemeUtil.setUserPreferredTheme(this);
|
||||
DeviceHelper.initializeDeviceId(this);
|
||||
RsaHelper.initialiseRsaKeys(this);
|
||||
SslHelper.initialiseCertificate(this);
|
||||
PluginFactory.initPluginInfo(this);
|
||||
NotificationHelper.initializeChannels(this);
|
||||
LifecycleHelper.initializeObserver();
|
||||
loadRememberedDevicesFromSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
Log.d("KdeConnect/Application", "onTerminate");
|
||||
super.onTerminate();
|
||||
}
|
||||
|
||||
public void addDeviceListChangedCallback(String key, DeviceListChangedCallback callback) {
|
||||
deviceListChangedCallbacks.put(key, callback);
|
||||
}
|
||||
|
||||
public void removeDeviceListChangedCallback(String key) {
|
||||
deviceListChangedCallbacks.remove(key);
|
||||
}
|
||||
|
||||
private void onDeviceListChanged() {
|
||||
for (DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) {
|
||||
callback.onDeviceListChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ConcurrentHashMap<String, Device> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
public Device getDevice(String id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return devices.get(id);
|
||||
}
|
||||
|
||||
public <T extends Plugin> T getDevicePlugin(String deviceId, Class<T> pluginClass) {
|
||||
if (deviceId == null) {
|
||||
return null;
|
||||
}
|
||||
Device device = devices.get(deviceId);
|
||||
if (device == null) {
|
||||
return null;
|
||||
}
|
||||
return device.getPlugin(pluginClass);
|
||||
}
|
||||
|
||||
public static KdeConnect getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void loadRememberedDevicesFromSettings() {
|
||||
//Log.e("BackgroundService", "Loading remembered trusted devices");
|
||||
SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
|
||||
Set<String> trustedDevices = preferences.getAll().keySet();
|
||||
for (String deviceId : trustedDevices) {
|
||||
//Log.e("BackgroundService", "Loading device "+deviceId);
|
||||
if (preferences.getBoolean(deviceId, false)) {
|
||||
Device device = new Device(this, deviceId);
|
||||
devices.put(deviceId, device);
|
||||
device.addPairingCallback(devicePairingCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Device.PairingCallback devicePairingCallback = new Device.PairingCallback() {
|
||||
@Override
|
||||
public void incomingRequest() {
|
||||
onDeviceListChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pairingSuccessful() {
|
||||
onDeviceListChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pairingFailed(String error) {
|
||||
onDeviceListChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unpaired() {
|
||||
onDeviceListChanged();
|
||||
}
|
||||
};
|
||||
|
||||
private final BaseLinkProvider.ConnectionReceiver connectionListener = new BaseLinkProvider.ConnectionReceiver() {
|
||||
@Override
|
||||
public void onConnectionReceived(final NetworkPacket identityPacket, final BaseLink link) {
|
||||
String deviceId = identityPacket.getString("deviceId");
|
||||
Device device = devices.get(deviceId);
|
||||
if (device != null) {
|
||||
Log.i("KDE/Application", "addLink, known device: " + deviceId);
|
||||
device.addLink(identityPacket, link);
|
||||
} else {
|
||||
Log.i("KDE/Application", "addLink,unknown device: " + deviceId);
|
||||
device = new Device(KdeConnect.this, identityPacket, link);
|
||||
devices.put(deviceId, device);
|
||||
device.addPairingCallback(devicePairingCallback);
|
||||
}
|
||||
onDeviceListChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionLost(BaseLink link) {
|
||||
Device d = devices.get(link.getDeviceId());
|
||||
Log.i("KDE/onConnectionLost", "removeLink, deviceId: " + link.getDeviceId());
|
||||
if (d != null) {
|
||||
d.removeLink(link);
|
||||
if (!d.isReachable() && !d.isPaired()) {
|
||||
//Log.e("onConnectionLost","Removing connection device because it was not paired");
|
||||
devices.remove(link.getDeviceId());
|
||||
d.removePairingCallback(devicePairingCallback);
|
||||
}
|
||||
} else {
|
||||
//Log.d("KDE/onConnectionLost","Removing connection to unknown device");
|
||||
}
|
||||
onDeviceListChanged();
|
||||
}
|
||||
};
|
||||
public BaseLinkProvider.ConnectionReceiver getConnectionListener() {
|
||||
return connectionListener;
|
||||
}
|
||||
|
||||
}
|
@@ -15,7 +15,6 @@ import android.util.Log;
|
||||
|
||||
public class KdeConnectBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
|
||||
//Log.e("KdeConnect", "Broadcast event: "+intent.getAction());
|
||||
@@ -25,9 +24,7 @@ public class KdeConnectBroadcastReceiver extends BroadcastReceiver {
|
||||
switch (action) {
|
||||
case Intent.ACTION_MY_PACKAGE_REPLACED:
|
||||
Log.i("KdeConnect", "MyUpdateReceiver");
|
||||
BackgroundService.RunCommand(context, service -> {
|
||||
|
||||
});
|
||||
BackgroundService.Start(context);
|
||||
break;
|
||||
case Intent.ACTION_PACKAGE_REPLACED:
|
||||
Log.i("KdeConnect", "UpdateReceiver");
|
||||
@@ -35,27 +32,20 @@ public class KdeConnectBroadcastReceiver extends BroadcastReceiver {
|
||||
Log.i("KdeConnect", "Ignoring, it's not me!");
|
||||
return;
|
||||
}
|
||||
BackgroundService.RunCommand(context, service -> {
|
||||
|
||||
});
|
||||
BackgroundService.Start(context);
|
||||
break;
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
Log.i("KdeConnect", "KdeConnectBroadcastReceiver");
|
||||
BackgroundService.RunCommand(context, service -> {
|
||||
|
||||
});
|
||||
BackgroundService.Start(context);
|
||||
break;
|
||||
case WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION:
|
||||
case WifiManager.WIFI_STATE_CHANGED_ACTION:
|
||||
case ConnectivityManager.CONNECTIVITY_ACTION:
|
||||
Log.i("KdeConnect", "Connection state changed, trying to connect");
|
||||
BackgroundService.RunCommand(context, service -> {
|
||||
service.onDeviceListChanged();
|
||||
service.onNetworkChange();
|
||||
});
|
||||
BackgroundService.ForceRefreshConnections(context);
|
||||
break;
|
||||
case Intent.ACTION_SCREEN_ON:
|
||||
BackgroundService.RunCommand(context, BackgroundService::onNetworkChange);
|
||||
BackgroundService.ForceRefreshConnections(context);
|
||||
break;
|
||||
default:
|
||||
Log.i("BroadcastReceiver", "Ignoring broadcast event: " + intent.getAction());
|
||||
|
@@ -40,12 +40,12 @@ public class BatteryPlugin extends Plugin {
|
||||
private DeviceBatteryInfo remoteBatteryInfo;
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getResources().getString(R.string.pref_plugin_battery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getResources().getString(R.string.pref_plugin_battery_desc);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public class BatteryPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
|
||||
if (np.getBoolean("request")) {
|
||||
device.sendPacket(batteryInfo);
|
||||
@@ -139,12 +139,12 @@ public class BatteryPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_BATTERY_REQUEST, PACKET_TYPE_BATTERY};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_BATTERY_REQUEST, PACKET_TYPE_BATTERY};
|
||||
}
|
||||
|
||||
|
@@ -17,10 +17,9 @@ import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.UserInterface.MainActivity;
|
||||
import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment;
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityBigscreenBinding;
|
||||
|
||||
@@ -49,28 +48,32 @@ public class BigscreenActivity extends AppCompatActivity {
|
||||
binding.micButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
BackgroundService.RunWithPlugin(this, deviceId, BigscreenPlugin.class, plugin -> runOnUiThread(() -> {
|
||||
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 -> {
|
||||
if (plugin.hasMicPermission()) {
|
||||
activateSTT();
|
||||
} else {
|
||||
new PermissionsAlertDialogFragment.Builder()
|
||||
.setTitle(plugin.getDisplayName())
|
||||
.setMessage(R.string.bigscreen_optional_permission_explanation)
|
||||
.setPositiveButton(R.string.ok)
|
||||
.setNegativeButton(R.string.cancel)
|
||||
.setPermissions(new String[]{Manifest.permission.RECORD_AUDIO})
|
||||
.setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
|
||||
.create().show(getSupportFragmentManager(), null);
|
||||
}
|
||||
});
|
||||
}));
|
||||
BigscreenPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, BigscreenPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
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 -> {
|
||||
if (plugin.hasMicPermission()) {
|
||||
activateSTT();
|
||||
} else {
|
||||
new PermissionsAlertDialogFragment.Builder()
|
||||
.setTitle(plugin.getDisplayName())
|
||||
.setMessage(R.string.bigscreen_optional_permission_explanation)
|
||||
.setPositiveButton(R.string.ok)
|
||||
.setNegativeButton(R.string.cancel)
|
||||
.setPermissions(new String[]{Manifest.permission.RECORD_AUDIO})
|
||||
.setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
|
||||
.create().show(getSupportFragmentManager(), null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void activateSTT() {
|
||||
@@ -89,9 +92,12 @@ public class BigscreenActivity extends AppCompatActivity {
|
||||
.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
|
||||
if (result.get(0) != null) {
|
||||
final String deviceId = getIntent().getStringExtra("deviceId");
|
||||
BackgroundService.RunWithPlugin(this, deviceId, BigscreenPlugin.class, plugin ->
|
||||
runOnUiThread(() -> plugin.sendSTT(result.get(0)))
|
||||
);
|
||||
BigscreenPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, BigscreenPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendSTT(result.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.kde.kdeconnect.Device;
|
||||
@@ -42,12 +43,12 @@ public class BigscreenPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getString(R.string.pref_plugin_bigscreen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getString(R.string.pref_plugin_bigscreen_desc);
|
||||
}
|
||||
|
||||
@@ -79,19 +80,19 @@ public class BigscreenPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_BIGSCREEN_STT}; }
|
||||
public @NonNull String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_BIGSCREEN_STT}; }
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MOUSEPAD_REQUEST, PACKET_TYPE_BIGSCREEN_STT};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActionName() {
|
||||
public @NonNull String getActionName() {
|
||||
return context.getString(R.string.pref_plugin_bigscreen);
|
||||
}
|
||||
|
||||
public String[] getOptionalPermissions() {
|
||||
public @NonNull String[] getOptionalPermissions() {
|
||||
return new String[]{Manifest.permission.RECORD_AUDIO};
|
||||
}
|
||||
|
||||
|
@@ -18,6 +18,7 @@ import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
@@ -54,17 +55,17 @@ public class ClipboardPlugin extends Plugin {
|
||||
private final static String PACKET_TYPE_CLIPBOARD_CONNECT = "kdeconnect.clipboard.connect";
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getResources().getString(R.string.pref_plugin_clipboard);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getResources().getString(R.string.pref_plugin_clipboard_desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
String content = np.getString("content");
|
||||
switch (np.getType()) {
|
||||
case (PACKET_TYPE_CLIPBOARD):
|
||||
@@ -114,17 +115,17 @@ public class ClipboardPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_CLIPBOARD, PACKET_TYPE_CLIPBOARD_CONNECT};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_CLIPBOARD, PACKET_TYPE_CLIPBOARD_CONNECT};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActionName() {
|
||||
public @NonNull String getActionName() {
|
||||
return context.getString(R.string.send_clipboard);
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.kde.kdeconnect.BackgroundService
|
||||
import org.kde.kdeconnect.KdeConnect
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class ClipboardTileService : TileService() {
|
||||
@@ -19,13 +19,9 @@ class ClipboardTileService : TileService() {
|
||||
|
||||
startActivityAndCollapse(Intent(this, ClipboardFloatingActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
var ids : List<String> = emptyList()
|
||||
val service = BackgroundService.getInstance()
|
||||
if (service != null) {
|
||||
ids = service.devices.values
|
||||
.filter { it.isReachable && it.isPaired }
|
||||
.map { it.deviceId }
|
||||
}
|
||||
val ids = KdeConnect.getInstance().devices.values
|
||||
.filter { it.isReachable && it.isPaired }
|
||||
.map { it.deviceId }
|
||||
putExtra("connectedDeviceIds", ArrayList(ids))
|
||||
})
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.json.JSONException;
|
||||
@@ -70,12 +71,12 @@ public class ConnectivityReportPlugin extends Plugin {
|
||||
private final HashMap<Integer, SubscriptionState> states = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getResources().getString(R.string.pref_plugin_connectivity_report);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getResources().getString(R.string.pref_plugin_connectivity_report_desc);
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ public class ConnectivityReportPlugin extends Plugin {
|
||||
|
||||
serializeSignalStrengths();
|
||||
device.sendPacket(connectivityInfo);
|
||||
Log.i("ConnectivityReport", "signalStrength of #" + subID + " updated to " + level);
|
||||
//Log.i("ConnectivityReport", "signalStrength of #" + subID + " updated to " + level);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -164,7 +165,7 @@ public class ConnectivityReportPlugin extends Plugin {
|
||||
|
||||
serializeSignalStrengths();
|
||||
device.sendPacket(connectivityInfo);
|
||||
Log.i("ConnectivityReport", "networkType of #" + subID + " updated to " + networkTypeToString(networkType));
|
||||
//Log.i("ConnectivityReport", "networkType of #" + subID + " updated to " + networkTypeToString(networkType));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -228,7 +229,7 @@ public class ConnectivityReportPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
if (PACKET_TYPE_CONNECTIVITY_REPORT_REQUEST.equals(np.getType())) {
|
||||
Log.i("ConnectivityReport", "Requested");
|
||||
serializeSignalStrengths();
|
||||
@@ -239,17 +240,17 @@ public class ConnectivityReportPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_CONNECTIVITY_REPORT_REQUEST};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_CONNECTIVITY_REPORT};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getRequiredPermissions() {
|
||||
public @NonNull String[] getRequiredPermissions() {
|
||||
return new String[]{
|
||||
Manifest.permission.READ_PHONE_STATE,
|
||||
};
|
||||
|
@@ -12,6 +12,8 @@ package org.kde.kdeconnect.Plugins.ContactsPlugin;
|
||||
import android.Manifest;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper;
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper.ContactNotFoundException;
|
||||
import org.kde.kdeconnect.Helpers.ContactsHelper.VCardBuilder;
|
||||
@@ -66,17 +68,17 @@ public class ContactsPlugin extends Plugin {
|
||||
private static final String PACKET_TYPE_CONTACTS_RESPONSE_VCARDS = "kdeconnect.contacts.response_vcards";
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getResources().getString(R.string.pref_plugin_contacts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getResources().getString(R.string.pref_plugin_contacts_desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{
|
||||
PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS,
|
||||
PACKET_TYPE_CONTACTS_REQUEST_VCARDS_BY_UIDS
|
||||
@@ -84,7 +86,7 @@ public class ContactsPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{
|
||||
PACKET_TYPE_CONTACTS_RESPONSE_UIDS_TIMESTAMPS,
|
||||
PACKET_TYPE_CONTACTS_RESPONSE_VCARDS
|
||||
@@ -102,7 +104,7 @@ public class ContactsPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getRequiredPermissions() {
|
||||
public @NonNull String[] getRequiredPermissions() {
|
||||
return new String[]{Manifest.permission.READ_CONTACTS};
|
||||
// One day maybe we will also support WRITE_CONTACTS, but not yet
|
||||
}
|
||||
@@ -214,7 +216,7 @@ public class ContactsPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
switch (np.getType()) {
|
||||
case PACKET_TYPE_CONTACTS_REQUEST_ALL_UIDS_TIMESTAMPS:
|
||||
return this.handleRequestAllUIDsTimestamps(np);
|
||||
|
@@ -12,8 +12,7 @@ import android.view.WindowManager;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityFindMyPhoneBinding;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -21,7 +20,7 @@ import java.util.Objects;
|
||||
public class FindMyPhoneActivity extends AppCompatActivity {
|
||||
static final String EXTRA_DEVICE_ID = "deviceId";
|
||||
|
||||
private FindMyPhonePlugin plugin;
|
||||
String deviceId;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -39,8 +38,7 @@ public class FindMyPhoneActivity extends AppCompatActivity {
|
||||
finish();
|
||||
}
|
||||
|
||||
String deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
|
||||
plugin = BackgroundService.getInstance().getDevice(deviceId).getPlugin(FindMyPhonePlugin.class);
|
||||
deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
|
||||
|
||||
Window window = this.getWindow();
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
|
||||
@@ -53,11 +51,10 @@ public class FindMyPhoneActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
/*
|
||||
For whatever reason when Android launches this activity as a SystemAlertWindow it calls:
|
||||
onCreate(), onStart(), onResume(), onStop(), onStart(), onResume().
|
||||
When using BackgroundService.RunWithPlugin we get into concurrency problems and sometimes no sound will be played
|
||||
*/
|
||||
FindMyPhonePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, FindMyPhonePlugin.class);
|
||||
if (plugin == null) {
|
||||
return;
|
||||
}
|
||||
plugin.startPlaying();
|
||||
plugin.hideNotification();
|
||||
}
|
||||
@@ -65,7 +62,10 @@ public class FindMyPhoneActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
FindMyPhonePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, FindMyPhonePlugin.class);
|
||||
if (plugin == null) {
|
||||
return;
|
||||
}
|
||||
plugin.stopPlaying();
|
||||
}
|
||||
}
|
||||
|
@@ -20,13 +20,14 @@ import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
||||
import org.kde.kdeconnect.Helpers.LifecycleHelper;
|
||||
import org.kde.kdeconnect.Helpers.NotificationHelper;
|
||||
import org.kde.kdeconnect.MyApplication;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
@@ -47,7 +48,7 @@ public class FindMyPhonePlugin extends Plugin {
|
||||
private PowerManager powerManager;
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
switch (DeviceHelper.getDeviceType(context)) {
|
||||
case Tv:
|
||||
return context.getString(R.string.findmyphone_title_tv);
|
||||
@@ -60,7 +61,7 @@ public class FindMyPhonePlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getString(R.string.findmyphone_description);
|
||||
}
|
||||
|
||||
@@ -106,8 +107,8 @@ public class FindMyPhonePlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || MyApplication.isInForeground()) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || LifecycleHelper.isInForeground()) {
|
||||
Intent intent = new Intent(context, FindMyPhoneActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(FindMyPhoneActivity.EXTRA_DEVICE_ID, device.getDeviceId());
|
||||
@@ -194,12 +195,12 @@ public class FindMyPhonePlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_FINDMYPHONE_REQUEST};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return ArrayUtils.EMPTY_STRING_ARRAY;
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
|
||||
public class FindMyPhoneReceiver extends BroadcastReceiver {
|
||||
final static String ACTION_FOUND_IT = "org.kde.kdeconnect.Plugins.FindMyPhonePlugin.foundIt";
|
||||
@@ -29,7 +29,10 @@ public class FindMyPhoneReceiver extends BroadcastReceiver {
|
||||
}
|
||||
|
||||
String deviceId = intent.getStringExtra(EXTRA_DEVICE_ID);
|
||||
|
||||
BackgroundService.RunWithPlugin(context, deviceId, FindMyPhonePlugin.class, FindMyPhonePlugin::stopPlaying);
|
||||
FindMyPhonePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, FindMyPhonePlugin.class);
|
||||
if (plugin == null) {
|
||||
return;
|
||||
}
|
||||
plugin.stopPlaying();
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,8 @@
|
||||
package org.kde.kdeconnect.Plugins.FindRemoteDevicePlugin;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
@@ -20,22 +21,22 @@ import org.kde.kdeconnect_tp.R;
|
||||
public class FindRemoteDevicePlugin extends Plugin {
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getResources().getString(R.string.pref_plugin_findremotedevice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getResources().getString(R.string.pref_plugin_findremotedevice_desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActionName() {
|
||||
public @NonNull String getActionName() {
|
||||
return context.getString(R.string.ring);
|
||||
}
|
||||
|
||||
@@ -46,23 +47,18 @@ public class FindRemoteDevicePlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMainActivity(Context context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean displayInContextMenu() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return ArrayUtils.EMPTY_STRING_ARRAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{FindMyPhonePlugin.PACKET_TYPE_FINDMYPHONE_REQUEST};
|
||||
}
|
||||
}
|
||||
|
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Forrest Hilton <forrestmhilton@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
|
||||
package org.kde.kdeconnect.Plugins.MousePadPlugin;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
public class ComposeSendActivity extends AppCompatActivity {
|
||||
|
||||
private String deviceId;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_compose_send);
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
||||
Intent intent = getIntent();
|
||||
|
||||
deviceId = intent.getStringExtra("org.kde.kdeconnect.Plugins.MousePadPlugin.deviceId");
|
||||
|
||||
EditText editText = findViewById(R.id.compose);
|
||||
|
||||
editText.requestFocus();
|
||||
// this is almost never used
|
||||
editText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||
sendComposed();
|
||||
return true;
|
||||
}
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
clear();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public void sendChars(CharSequence chars) {
|
||||
final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("key", chars.toString());
|
||||
sendKeyPressPacket(np);
|
||||
}
|
||||
|
||||
private void sendKeyPressPacket(final NetworkPacket np) {
|
||||
try {
|
||||
Log.d("packed", np.serialize());
|
||||
} catch (Exception e) {
|
||||
Log.e("KDE/ComposeSend", "Exception", e);
|
||||
}
|
||||
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendKeyboardPacket(np));
|
||||
}
|
||||
|
||||
public void sendComposed() {
|
||||
EditText editText = findViewById(R.id.compose);
|
||||
|
||||
String editTextStr = editText.getText().toString();
|
||||
sendChars(editTextStr);
|
||||
clear();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
EditText editText = findViewById(R.id.compose);
|
||||
editText.setText("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_compose_send, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.menu_clear_compose) {
|
||||
clear();
|
||||
return true;
|
||||
} else if (id == R.id.menu_send_compose) {
|
||||
sendComposed();
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Dmitry Yudin <dgyudin@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.MousePadPlugin
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import org.kde.kdeconnect.KdeConnect
|
||||
import org.kde.kdeconnect.NetworkPacket
|
||||
import org.kde.kdeconnect.UserInterface.compose.KdeTextButton
|
||||
import org.kde.kdeconnect.UserInterface.compose.KdeTextField
|
||||
import org.kde.kdeconnect.UserInterface.compose.KdeTopAppBar
|
||||
import org.kde.kdeconnect_tp.R
|
||||
|
||||
private const val INPUT_CACHE_KEY = "compose_send_input_cache"
|
||||
|
||||
class ComposeSendActivity : AppCompatActivity() {
|
||||
private var deviceId: String? = null
|
||||
private val userInput = mutableStateOf(String())
|
||||
private val prefs by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
prefs.getString(INPUT_CACHE_KEY, null)?.let { userInput.value = it }
|
||||
|
||||
setContent { ComposeSendScreen() }
|
||||
|
||||
deviceId = intent.getStringExtra("org.kde.kdeconnect.Plugins.MousePadPlugin.deviceId")
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
with(prefs.edit()) {
|
||||
if (userInput.value.isNotBlank()) putString(INPUT_CACHE_KEY, userInput.value) else remove(INPUT_CACHE_KEY)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendChars(chars: String) {
|
||||
val np = NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST)
|
||||
np["key"] = chars
|
||||
sendKeyPressPacket(np)
|
||||
}
|
||||
|
||||
private fun sendKeyPressPacket(np: NetworkPacket) {
|
||||
try {
|
||||
Log.d("packed", np.serialize())
|
||||
} catch (e: Exception) {
|
||||
Log.e("KDE/ComposeSend", "Exception", e)
|
||||
}
|
||||
val plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin::class.java)
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendKeyboardPacket(np);
|
||||
}
|
||||
|
||||
private fun sendComposed() {
|
||||
sendChars(userInput.value)
|
||||
clearComposeInput()
|
||||
}
|
||||
|
||||
private fun clearComposeInput() {
|
||||
userInput.value = String()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ComposeSendScreen() {
|
||||
Mdc3Theme {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
KdeTopAppBar(
|
||||
title = stringResource(R.string.compose_send_title),
|
||||
navIcon = Icons.Default.ArrowBack,
|
||||
navIconOnClick = { onBackPressedDispatcher.onBackPressed() },
|
||||
actions = {
|
||||
KdeTextButton(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
onClick = { clearComposeInput() },
|
||||
text = stringResource(R.string.clear_compose),
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
) { scaffoldPadding ->
|
||||
Box(modifier = Modifier.padding(scaffoldPadding).fillMaxSize()) {
|
||||
KdeTextField(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 80.dp)
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth(),
|
||||
input = userInput,
|
||||
label = stringResource(R.string.click_here_to_type),
|
||||
)
|
||||
KdeTextButton(
|
||||
onClick = { sendComposed() },
|
||||
modifier = Modifier.padding(all = 16.dp).align(Alignment.BottomEnd),
|
||||
enabled = userInput.value.isNotEmpty(),
|
||||
text = stringResource(R.string.send_compose),
|
||||
iconLeft = Icons.Default.Send,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,7 +14,7 @@ import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
|
||||
public class KeyListenerView extends View {
|
||||
@@ -89,7 +89,11 @@ public class KeyListenerView extends View {
|
||||
}
|
||||
|
||||
private void sendKeyPressPacket(final NetworkPacket np) {
|
||||
BackgroundService.RunWithPlugin(getContext(), deviceId, MousePadPlugin.class, plugin -> plugin.sendKeyboardPacket(np));
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
return;
|
||||
}
|
||||
plugin.sendKeyboardPacket(np);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -8,6 +8,10 @@ package org.kde.kdeconnect.Plugins.MousePadPlugin;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
@@ -15,25 +19,27 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.hardware.SensorManager;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.Sensor;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.UserInterface.PluginSettingsActivity;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class MousePadActivity extends AppCompatActivity implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, MousePadGestureDetector.OnGestureListener, SensorEventListener {
|
||||
public class MousePadActivity
|
||||
extends AppCompatActivity
|
||||
implements GestureDetector.OnGestureListener,
|
||||
GestureDetector.OnDoubleTapListener,
|
||||
MousePadGestureDetector.OnGestureListener,
|
||||
SensorEventListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private String deviceId;
|
||||
|
||||
private final static float MinDistanceToSendScroll = 2.5f; // touch gesture scroll
|
||||
@@ -49,6 +55,7 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
private int scrollDirection = 1;
|
||||
private boolean allowGyro = false;
|
||||
private boolean gyroEnabled = false;
|
||||
private int gyroscopeSensitivity = 100;
|
||||
private boolean isScrolling = false;
|
||||
private float accumulatedDistanceY = 0;
|
||||
|
||||
@@ -63,6 +70,8 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
|
||||
private SharedPreferences prefs = null;
|
||||
|
||||
private boolean prefsApplied = false;
|
||||
|
||||
enum ClickType {
|
||||
LEFT, RIGHT, MIDDLE, NONE;
|
||||
|
||||
@@ -90,27 +99,30 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
float[] values = event.values;
|
||||
|
||||
float X = -values[2] * 70 * mCurrentSensitivity * displayDpiMultiplier;
|
||||
float Y = -values[0] * 70 * mCurrentSensitivity * displayDpiMultiplier;
|
||||
float X = -values[2] * 70 * (gyroscopeSensitivity/100.0f);
|
||||
float Y = -values[0] * 70 * (gyroscopeSensitivity/100.0f);
|
||||
|
||||
if (X < 0.25 && X > -0.25) {
|
||||
X = 0;
|
||||
} else {
|
||||
X = X * mCurrentSensitivity * displayDpiMultiplier;
|
||||
X = X * (gyroscopeSensitivity/100.0f);
|
||||
}
|
||||
|
||||
if (Y < 0.25 && Y > -0.25) {
|
||||
Y = 0;
|
||||
} else {
|
||||
Y = Y * mCurrentSensitivity * displayDpiMultiplier;
|
||||
Y = Y * (gyroscopeSensitivity/100.0f);
|
||||
}
|
||||
|
||||
final float nX = X;
|
||||
final float nY = Y;
|
||||
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> {
|
||||
plugin.sendMouseDelta(nX, nY);
|
||||
});
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendMouseDelta(nX, nY);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,6 +135,10 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
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());
|
||||
|
||||
deviceId = getIntent().getStringExtra("deviceId");
|
||||
|
||||
getWindow().getDecorView().setHapticFeedbackEnabled(true);
|
||||
@@ -136,57 +152,13 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
keyListenerView.setDeviceId(deviceId);
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
prefs.registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
if (prefs.getBoolean(getString(R.string.mousepad_scroll_direction), false)) {
|
||||
scrollDirection = -1;
|
||||
} else {
|
||||
scrollDirection = 1;
|
||||
}
|
||||
if (mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null
|
||||
&& prefs.getBoolean(getString(R.string.gyro_mouse_enabled), false)) {
|
||||
allowGyro = true;
|
||||
}
|
||||
String singleTapSetting = prefs.getString(getString(R.string.mousepad_single_tap_key),
|
||||
getString(R.string.mousepad_default_single));
|
||||
String doubleTapSetting = prefs.getString(getString(R.string.mousepad_double_tap_key),
|
||||
getString(R.string.mousepad_default_double));
|
||||
String tripleTapSetting = prefs.getString(getString(R.string.mousepad_triple_tap_key),
|
||||
getString(R.string.mousepad_default_triple));
|
||||
String sensitivitySetting = prefs.getString(getString(R.string.mousepad_sensitivity_key),
|
||||
getString(R.string.mousepad_default_sensitivity));
|
||||
|
||||
String accelerationProfileName = prefs.getString(getString(R.string.mousepad_acceleration_profile_key),
|
||||
getString(R.string.mousepad_default_acceleration_profile));
|
||||
|
||||
mPointerAccelerationProfile = PointerAccelerationProfileFactory.getProfileWithName(accelerationProfileName);
|
||||
|
||||
singleTapAction = ClickType.fromString(singleTapSetting);
|
||||
doubleTapAction = ClickType.fromString(doubleTapSetting);
|
||||
tripleTapAction = ClickType.fromString(tripleTapSetting);
|
||||
applyPrefs();
|
||||
|
||||
//Technically xdpi and ydpi should be handled separately,
|
||||
//but since ydpi is usually almost equal to xdpi, only xdpi is used for the multiplier.
|
||||
displayDpiMultiplier = StandardDpi / getResources().getDisplayMetrics().xdpi;
|
||||
switch (sensitivitySetting) {
|
||||
case "slowest":
|
||||
mCurrentSensitivity = 0.2f;
|
||||
break;
|
||||
case "aboveSlowest":
|
||||
mCurrentSensitivity = 0.5f;
|
||||
break;
|
||||
case "default":
|
||||
mCurrentSensitivity = 1.0f;
|
||||
break;
|
||||
case "aboveDefault":
|
||||
mCurrentSensitivity = 1.5f;
|
||||
break;
|
||||
case "fastest":
|
||||
mCurrentSensitivity = 2.0f;
|
||||
break;
|
||||
default:
|
||||
mCurrentSensitivity = 1.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
final View decorView = getWindow().getDecorView();
|
||||
decorView.setOnSystemUiVisibilityChangeListener(visibility -> {
|
||||
@@ -205,19 +177,13 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
applyPrefs();
|
||||
|
||||
if (allowGyro && !gyroEnabled) {
|
||||
mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManager.SENSOR_DELAY_GAME);
|
||||
gyroEnabled = true;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean(getString(R.string.mousepad_mouse_buttons_enabled_pref), true)) {
|
||||
findViewById(R.id.mouse_buttons).setVisibility(View.VISIBLE);
|
||||
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());
|
||||
} else {
|
||||
findViewById(R.id.mouse_buttons).setVisibility(View.GONE);
|
||||
}
|
||||
invalidateMenu();
|
||||
|
||||
super.onResume();
|
||||
@@ -232,7 +198,8 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override protected void onStop() {
|
||||
@Override
|
||||
protected void onStop() {
|
||||
if (gyroEnabled) {
|
||||
mSensorManager.unregisterListener(this);
|
||||
gyroEnabled = false;
|
||||
@@ -240,6 +207,12 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
@@ -269,24 +242,30 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
startActivity(intent);
|
||||
return true;
|
||||
} else if (id == R.id.menu_show_keyboard) {
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> {
|
||||
if (plugin.isKeyboardEnabled()) {
|
||||
showKeyboard();
|
||||
} else {
|
||||
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT);
|
||||
toast.show();
|
||||
}
|
||||
});
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
if (plugin.isKeyboardEnabled()) {
|
||||
showKeyboard();
|
||||
} else {
|
||||
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT);
|
||||
toast.show();
|
||||
}
|
||||
return true;
|
||||
} else if (id == R.id.menu_open_compose_send) {
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> {
|
||||
if (plugin.isKeyboardEnabled()) {
|
||||
showCompose();
|
||||
} else {
|
||||
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT);
|
||||
toast.show();
|
||||
}
|
||||
});
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
if (plugin.isKeyboardEnabled()) {
|
||||
showCompose();
|
||||
} else {
|
||||
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT);
|
||||
toast.show();
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
@@ -322,20 +301,23 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
mCurrentX = event.getX();
|
||||
mCurrentY = event.getY();
|
||||
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> {
|
||||
float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity;
|
||||
float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity;
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run the mouse delta through the pointer acceleration profile
|
||||
mPointerAccelerationProfile.touchMoved(deltaX, deltaY, event.getEventTime());
|
||||
mouseDelta = mPointerAccelerationProfile.commitAcceleratedMouseDelta(mouseDelta);
|
||||
float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity;
|
||||
float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity;
|
||||
|
||||
plugin.sendMouseDelta(mouseDelta.x, mouseDelta.y);
|
||||
// Run the mouse delta through the pointer acceleration profile
|
||||
mPointerAccelerationProfile.touchMoved(deltaX, deltaY, event.getEventTime());
|
||||
mouseDelta = mPointerAccelerationProfile.commitAcceleratedMouseDelta(mouseDelta);
|
||||
|
||||
mPrevX = mCurrentX;
|
||||
mPrevY = mCurrentY;
|
||||
});
|
||||
plugin.sendMouseDelta(mouseDelta.x, mouseDelta.y);
|
||||
|
||||
mPrevX = mCurrentX;
|
||||
mPrevY = mCurrentY;
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -395,7 +377,12 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleHold);
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendSingleHold();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -422,7 +409,12 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendDoubleClick);
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
plugin.sendDoubleClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -465,24 +457,48 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (prefsApplied) prefsApplied = false;
|
||||
}
|
||||
|
||||
|
||||
private void sendLeftClick() {
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendLeftClick);
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendLeftClick();
|
||||
}
|
||||
|
||||
private void sendMiddleClick() {
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendMiddleClick);
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendMiddleClick();
|
||||
}
|
||||
|
||||
private void sendRightClick() {
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendRightClick);
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendRightClick();
|
||||
}
|
||||
|
||||
private void sendScroll(final float y) {
|
||||
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendScroll(0, y));
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendScroll(0, y);
|
||||
}
|
||||
|
||||
//TODO: Does not work on KitKat with or without requestFocus()
|
||||
private void showKeyboard() {
|
||||
InputMethodManager imm = ContextCompat.getSystemService(this, InputMethodManager.class);
|
||||
keyListenerView.requestFocus();
|
||||
@@ -495,6 +511,70 @@ public class MousePadActivity extends AppCompatActivity implements GestureDetect
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void applyPrefs() {
|
||||
if (prefsApplied) return;
|
||||
|
||||
if (prefs.getBoolean(getString(R.string.mousepad_scroll_direction), false)) {
|
||||
scrollDirection = -1;
|
||||
} else {
|
||||
scrollDirection = 1;
|
||||
}
|
||||
|
||||
allowGyro = isGyroSensorAvailable() && prefs.getBoolean(getString(R.string.gyro_mouse_enabled), false);
|
||||
if (allowGyro) gyroscopeSensitivity = prefs.getInt(getString(R.string.gyro_mouse_sensitivity), 100);
|
||||
|
||||
String singleTapSetting = prefs.getString(getString(R.string.mousepad_single_tap_key),
|
||||
getString(R.string.mousepad_default_single));
|
||||
String doubleTapSetting = prefs.getString(getString(R.string.mousepad_double_tap_key),
|
||||
getString(R.string.mousepad_default_double));
|
||||
String tripleTapSetting = prefs.getString(getString(R.string.mousepad_triple_tap_key),
|
||||
getString(R.string.mousepad_default_triple));
|
||||
String sensitivitySetting = prefs.getString(getString(R.string.mousepad_sensitivity_key),
|
||||
getString(R.string.mousepad_default_sensitivity));
|
||||
|
||||
String accelerationProfileName = prefs.getString(getString(R.string.mousepad_acceleration_profile_key),
|
||||
getString(R.string.mousepad_default_acceleration_profile));
|
||||
|
||||
mPointerAccelerationProfile = PointerAccelerationProfileFactory.getProfileWithName(accelerationProfileName);
|
||||
|
||||
singleTapAction = ClickType.fromString(singleTapSetting);
|
||||
doubleTapAction = ClickType.fromString(doubleTapSetting);
|
||||
tripleTapAction = ClickType.fromString(tripleTapSetting);
|
||||
|
||||
switch (sensitivitySetting) {
|
||||
case "slowest":
|
||||
mCurrentSensitivity = 0.2f;
|
||||
break;
|
||||
case "aboveSlowest":
|
||||
mCurrentSensitivity = 0.5f;
|
||||
break;
|
||||
case "default":
|
||||
mCurrentSensitivity = 1.0f;
|
||||
break;
|
||||
case "aboveDefault":
|
||||
mCurrentSensitivity = 1.5f;
|
||||
break;
|
||||
case "fastest":
|
||||
mCurrentSensitivity = 2.0f;
|
||||
break;
|
||||
default:
|
||||
mCurrentSensitivity = 1.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean(getString(R.string.mousepad_mouse_buttons_enabled_pref), true)) {
|
||||
findViewById(R.id.mouse_buttons).setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
findViewById(R.id.mouse_buttons).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
prefsApplied = true;
|
||||
}
|
||||
|
||||
private boolean isGyroSensorAvailable() {
|
||||
return mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
super.onBackPressed();
|
||||
|
@@ -11,6 +11,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
@@ -29,7 +30,7 @@ public class MousePadPlugin extends Plugin {
|
||||
private boolean keyboardEnabled = true;
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
|
||||
keyboardEnabled = np.getBoolean("state", true);
|
||||
|
||||
@@ -37,12 +38,12 @@ public class MousePadPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getString(R.string.pref_plugin_mousepad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getString(R.string.pref_plugin_mousepad_desc);
|
||||
}
|
||||
|
||||
@@ -74,17 +75,17 @@ public class MousePadPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MOUSEPAD_REQUEST};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActionName() {
|
||||
public @NonNull String getActionName() {
|
||||
return context.getString(R.string.open_mousepad);
|
||||
}
|
||||
|
||||
@@ -93,6 +94,7 @@ public class MousePadPlugin extends Plugin {
|
||||
if (np == null) {
|
||||
np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
} else {
|
||||
// TODO: In my tests we never get here. Decide if it's worth keeping the logic to replace unsent packets.
|
||||
dx += np.getInt("dx");
|
||||
dy += np.getInt("dx");
|
||||
}
|
||||
|
@@ -19,11 +19,11 @@ 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.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.UserInterface.ThemeUtil;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivitySendkeystrokesBinding;
|
||||
|
||||
@@ -89,7 +89,7 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
|
||||
// If we trust the sending app, check if there is only one device paired / reachable...
|
||||
if (contentIsOkay) {
|
||||
List<Device> reachableDevices = BackgroundService.getInstance().getDevices().values().stream()
|
||||
List<Device> reachableDevices = KdeConnect.getInstance().getDevices().values().stream()
|
||||
.filter(Device::isReachable)
|
||||
.limit(2) // we only need the first two; if its more than one, we need to show the user the device-selection
|
||||
.collect(Collectors.toList());
|
||||
@@ -103,16 +103,9 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// subscribe to new connected devices
|
||||
BackgroundService.RunCommand(this, service -> {
|
||||
service.onNetworkChange();
|
||||
service.addDeviceListChangedCallback("SendKeystrokesToHostActivity", unused -> updateDeviceList());
|
||||
});
|
||||
|
||||
// list all currently connected devices
|
||||
KdeConnect.getInstance().addDeviceListChangedCallback("SendKeystrokesToHostActivity", () -> runOnUiThread(this::updateDeviceList));
|
||||
BackgroundService.ForceRefreshConnections(this); // force a network re-discover
|
||||
updateDeviceList();
|
||||
|
||||
} else {
|
||||
Toast.makeText(getApplicationContext(), R.string.sendkeystrokes_wrong_data, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
@@ -122,7 +115,7 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("SendKeystrokesToHostActivity"));
|
||||
KdeConnect.getInstance().removeDeviceListChangedCallback("SendKeystrokesToHostActivity");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@@ -131,7 +124,12 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
if (binding.textToSend.getText() != null && (toSend = binding.textToSend.getText().toString().trim()).length() > 0) {
|
||||
final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("key", toSend);
|
||||
BackgroundService.RunWithPlugin(this, deviceId.getDeviceId(), MousePadPlugin.class, plugin -> plugin.sendKeyboardPacket(np));
|
||||
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId.getDeviceId(), MousePadPlugin.class);
|
||||
if (plugin == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
plugin.sendKeyboardPacket(np);
|
||||
Toast.makeText(
|
||||
getApplicationContext(),
|
||||
getString(R.string.sendkeystrokes_sent_text, toSend, deviceId.getName()),
|
||||
@@ -143,41 +141,37 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
|
||||
|
||||
private void updateDeviceList() {
|
||||
BackgroundService.RunCommand(this, service -> {
|
||||
Collection<Device> devices = KdeConnect.getInstance().getDevices().values();
|
||||
final ArrayList<Device> devicesList = new ArrayList<>();
|
||||
final ArrayList<ListAdapter.Item> items = new ArrayList<>();
|
||||
|
||||
Collection<Device> devices = service.getDevices().values();
|
||||
final ArrayList<Device> devicesList = new ArrayList<>();
|
||||
final ArrayList<ListAdapter.Item> items = new ArrayList<>();
|
||||
SectionItem section = new SectionItem(getString(R.string.sendkeystrokes_send_to));
|
||||
items.add(section);
|
||||
|
||||
SectionItem section = new SectionItem(getString(R.string.sendkeystrokes_send_to));
|
||||
items.add(section);
|
||||
|
||||
for (Device d : devices) {
|
||||
if (d.isReachable() && d.isPaired()) {
|
||||
devicesList.add(d);
|
||||
items.add(new EntryItemWithIcon(d.getName(), d.getIcon()));
|
||||
section.isEmpty = false;
|
||||
}
|
||||
for (Device d : devices) {
|
||||
if (d.isReachable() && d.isPaired()) {
|
||||
devicesList.add(d);
|
||||
items.add(new EntryItemWithIcon(d.getName(), d.getIcon()));
|
||||
section.isEmpty = false;
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
binding.devicesList.setAdapter(new ListAdapter(SendKeystrokesToHostActivity.this, items));
|
||||
binding.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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// only one device is connected and we trust the text to send -> send it and close the activity.
|
||||
// Usually we already check it in `onStart` - but if the BackgroundService was not started/connected to the host
|
||||
// it will not have the deviceList in memory. Use this callback as second chance (but it will flicker a bit, because the activity might
|
||||
// already been visible and get closed again quickly)
|
||||
if (devicesList.size() == 1 && contentIsOkay) {
|
||||
Device device = devicesList.get(0);
|
||||
sendKeys(device);
|
||||
this.finish(); // close the activity
|
||||
}
|
||||
binding.devicesList.setAdapter(new ListAdapter(SendKeystrokesToHostActivity.this, items));
|
||||
binding.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
|
||||
});
|
||||
|
||||
// only one device is connected and we trust the text to send -> send it and close the activity.
|
||||
// Usually we already check it in `onStart` - but if the BackgroundService was not started/connected to the host
|
||||
// it will not have the deviceList in memory. Use this callback as second chance (but it will flicker a bit, because the activity might
|
||||
// already been visible and get closed again quickly)
|
||||
if (devicesList.size() == 1 && contentIsOkay) {
|
||||
Device device = devicesList.get(0);
|
||||
sendKeys(device);
|
||||
this.finish(); // close the activity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,18 +10,19 @@ import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.Plugins.Plugin;
|
||||
import org.kde.kdeconnect.Plugins.PluginFactory;
|
||||
import org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin.RemoteKeyboardPlugin;
|
||||
import org.kde.kdeconnect.UserInterface.MainActivity;
|
||||
import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
@PluginFactory.LoadablePlugin
|
||||
//@PluginFactory.LoadablePlugin
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public class MouseReceiverPlugin extends Plugin {
|
||||
private final static String PACKET_TYPE_MOUSEPAD_REQUEST = "kdeconnect.mousepad.request";
|
||||
@@ -38,7 +39,7 @@ public class MouseReceiverPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DialogFragment getPermissionExplanationDialog() {
|
||||
public @NonNull DialogFragment getPermissionExplanationDialog() {
|
||||
return new StartActivityAlertDialogFragment.Builder()
|
||||
.setTitle(R.string.mouse_receiver_plugin_description)
|
||||
.setMessage(R.string.mouse_receiver_no_permissions)
|
||||
@@ -51,13 +52,7 @@ public class MouseReceiverPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.e("MouseReceiverPlugin", "onDestroy()");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
if (!np.getType().equals(PACKET_TYPE_MOUSEPAD_REQUEST)) {
|
||||
Log.e("MouseReceiverPlugin", "Invalid packet type for MouseReceiverPlugin: " + np.getType());
|
||||
return false;
|
||||
@@ -133,22 +128,22 @@ public class MouseReceiverPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getString(R.string.mouse_receiver_plugin_name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getString(R.string.mouse_receiver_plugin_description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MOUSEPAD_REQUEST};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
return new String[0];
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return ArrayUtils.EMPTY_STRING_ARRAY;
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.ConnectivityManager
|
||||
@@ -20,6 +19,7 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.kde.kdeconnect.NetworkPacket.Payload
|
||||
import org.kde.kdeconnect_tp.BuildConfig
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -78,14 +78,9 @@ internal object AlbumArtCache {
|
||||
fun initializeDiskCache(context: Context) {
|
||||
if (this::diskCache.isInitialized) return
|
||||
val cacheDir = File(context.cacheDir, "album_art")
|
||||
val versionCode: Int
|
||||
try {
|
||||
val info = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
versionCode = info.versionCode
|
||||
//Initialize the disk cache with a limit of 5 MB storage (fits ~830 images, taking Spotify as reference)
|
||||
diskCache = DiskLruCache.open(cacheDir, versionCode, 1, 1000 * 1000 * 5.toLong())
|
||||
} catch (e: NameNotFoundException) {
|
||||
throw AssertionError(e)
|
||||
diskCache = DiskLruCache.open(cacheDir, BuildConfig.VERSION_CODE, 1, 1000 * 1000 * 5.toLong())
|
||||
} catch (e: IOException) {
|
||||
Log.e("KDE/Mpris/AlbumArtCache", "Could not open the album art disk cache!", e)
|
||||
}
|
||||
|
@@ -11,8 +11,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
|
||||
/**
|
||||
* Called when the mpris media notification's buttons are pressed
|
||||
@@ -29,7 +28,7 @@ public class MprisMediaNotificationReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
//First case: buttons send by other applications via the media session APIs
|
||||
//First case: buttons send by other applications via the media session APIs. They don't target a specific device.
|
||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
|
||||
//Route these buttons to the media session, which will handle them
|
||||
MediaSessionCompat mediaSession = MprisMediaSession.getMediaSession();
|
||||
@@ -39,13 +38,10 @@ public class MprisMediaNotificationReceiver extends BroadcastReceiver {
|
||||
//Second case: buttons on the notification, which we created ourselves
|
||||
|
||||
//Get the correct device, the mpris plugin and the mpris player
|
||||
BackgroundService service = BackgroundService.getInstance();
|
||||
if (service == null) return;
|
||||
Device device = service.getDevice(intent.getStringExtra(EXTRA_DEVICE_ID));
|
||||
if (device == null) return;
|
||||
MprisPlugin mpris = device.getPlugin(MprisPlugin.class);
|
||||
if (mpris == null) return;
|
||||
MprisPlugin.MprisPlayer player = mpris.getPlayerStatus(intent.getStringExtra(EXTRA_MPRIS_PLAYER));
|
||||
String deviceId = intent.getStringExtra(EXTRA_DEVICE_ID);
|
||||
MprisPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MprisPlugin.class);
|
||||
if (plugin == null) return;
|
||||
MprisPlugin.MprisPlayer player = plugin.getPlayerStatus(intent.getStringExtra(EXTRA_MPRIS_PLAYER));
|
||||
if (player == null) return;
|
||||
|
||||
//Forward the action to the player
|
||||
@@ -65,7 +61,9 @@ public class MprisMediaNotificationReceiver extends BroadcastReceiver {
|
||||
case ACTION_CLOSE_NOTIFICATION:
|
||||
//The user dismissed the notification: actually handle its removal correctly
|
||||
MprisMediaSession.getInstance().closeMediaNotification();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
@@ -27,9 +28,9 @@ import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.media.app.NotificationCompat.MediaStyle;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.Helpers.NotificationHelper;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationReceiver;
|
||||
import org.kde.kdeconnect.Plugins.SystemVolumePlugin.SystemVolumePlugin;
|
||||
import org.kde.kdeconnect.Plugins.SystemVolumePlugin.SystemVolumeProvider;
|
||||
@@ -112,20 +113,20 @@ public class MprisMediaSession implements
|
||||
* <p>
|
||||
* Can be called multiple times, once for each device
|
||||
*
|
||||
* @param _context The context
|
||||
* @param mpris The mpris plugin
|
||||
* @param device The device id
|
||||
* @param context The context
|
||||
* @param plugin The mpris plugin
|
||||
* @param device The device id
|
||||
*/
|
||||
public void onCreate(Context _context, MprisPlugin mpris, String device) {
|
||||
public void onCreate(Context context, MprisPlugin plugin, String device) {
|
||||
if (mprisDevices.isEmpty()) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(_context);
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
context = _context;
|
||||
this.context = context;
|
||||
mprisDevices.add(device);
|
||||
|
||||
mpris.setPlayerListUpdatedHandler("media_notification", this::updateMediaNotification);
|
||||
mpris.setPlayerStatusUpdatedHandler("media_notification", this::updateMediaNotification);
|
||||
plugin.setPlayerListUpdatedHandler("media_notification", this::updateMediaNotification);
|
||||
plugin.setPlayerStatusUpdatedHandler("media_notification", this::updateMediaNotification);
|
||||
|
||||
NotificationReceiver.RunCommand(context, service -> {
|
||||
|
||||
@@ -137,8 +138,6 @@ public class MprisMediaSession implements
|
||||
onListenerConnected(service);
|
||||
}
|
||||
});
|
||||
|
||||
updateMediaNotification();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,13 +145,13 @@ public class MprisMediaSession implements
|
||||
* <p>
|
||||
* Can be called multiple times, once for each device
|
||||
*
|
||||
* @param mpris The mpris plugin
|
||||
* @param plugin The mpris plugin
|
||||
* @param device The device id
|
||||
*/
|
||||
public void onDestroy(MprisPlugin mpris, String device) {
|
||||
public void onDestroy(MprisPlugin plugin, String device) {
|
||||
mprisDevices.remove(device);
|
||||
mpris.removePlayerStatusUpdatedHandler("media_notification");
|
||||
mpris.removePlayerListUpdatedHandler("media_notification");
|
||||
plugin.removePlayerStatusUpdatedHandler("media_notification");
|
||||
plugin.removePlayerListUpdatedHandler("media_notification");
|
||||
updateMediaNotification();
|
||||
|
||||
if (mprisDevices.isEmpty()) {
|
||||
@@ -166,21 +165,19 @@ public class MprisMediaSession implements
|
||||
* <p>
|
||||
* Prefers playing devices/mpris players, but tries to keep displaying the same
|
||||
* player and device, while possible.
|
||||
*
|
||||
* @param service The background service
|
||||
*/
|
||||
private void updateCurrentPlayer(BackgroundService service) {
|
||||
Pair<Device, MprisPlugin.MprisPlayer> player = findPlayer(service);
|
||||
private void updateCurrentPlayer() {
|
||||
Pair<Device, MprisPlugin.MprisPlayer> player = findPlayer();
|
||||
|
||||
//Update the last-displayed device and player
|
||||
notificationDevice = player.first == null ? null : player.first.getDeviceId();
|
||||
notificationPlayer = player.second;
|
||||
}
|
||||
|
||||
private Pair<Device, MprisPlugin.MprisPlayer> findPlayer(BackgroundService service) {
|
||||
private Pair<Device, MprisPlugin.MprisPlayer> findPlayer() {
|
||||
//First try the previously displayed player (if still playing) or the previous displayed device (otherwise)
|
||||
if (notificationDevice != null && mprisDevices.contains(notificationDevice)) {
|
||||
Device device = service.getDevice(notificationDevice);
|
||||
Device device = KdeConnect.getInstance().getDevice(notificationDevice);
|
||||
|
||||
MprisPlugin.MprisPlayer player;
|
||||
if (notificationPlayer != null && notificationPlayer.isPlaying()) {
|
||||
@@ -194,7 +191,7 @@ public class MprisMediaSession implements
|
||||
}
|
||||
|
||||
// Try a different player from another device
|
||||
for (Device otherDevice : service.getDevices().values()) {
|
||||
for (Device otherDevice : KdeConnect.getInstance().getDevices().values()) {
|
||||
MprisPlugin.MprisPlayer player = getPlayerFromDevice(otherDevice, null);
|
||||
if (player != null) {
|
||||
return new Pair<>(otherDevice, player);
|
||||
@@ -205,7 +202,7 @@ public class MprisMediaSession implements
|
||||
// This will succeed if it's paused:
|
||||
// that allows pausing and subsequently resuming via the notification
|
||||
if (notificationDevice != null && mprisDevices.contains(notificationDevice)) {
|
||||
Device device = service.getDevice(notificationDevice);
|
||||
Device device = KdeConnect.getInstance().getDevice(notificationDevice);
|
||||
|
||||
MprisPlugin.MprisPlayer player = getPlayerFromDevice(device, notificationPlayer);
|
||||
if (player != null) {
|
||||
@@ -244,212 +241,212 @@ public class MprisMediaSession implements
|
||||
}
|
||||
|
||||
private void updateRemoteDeviceVolumeControl() {
|
||||
// Volume control feature is only available from Lollipop onwards
|
||||
BackgroundService.RunWithPlugin(context, notificationDevice, SystemVolumePlugin.class, plugin -> {
|
||||
SystemVolumeProvider systemVolumeProvider = SystemVolumeProvider.fromPlugin(plugin);
|
||||
systemVolumeProvider.addStateListener(this);
|
||||
systemVolumeProvider.startTrackingVolumeKeys();
|
||||
});
|
||||
SystemVolumePlugin plugin = KdeConnect.getInstance().getDevicePlugin(notificationDevice, SystemVolumePlugin.class);
|
||||
if (plugin == null) {
|
||||
return;
|
||||
}
|
||||
SystemVolumeProvider systemVolumeProvider = SystemVolumeProvider.fromPlugin(plugin);
|
||||
systemVolumeProvider.addStateListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the media control notification
|
||||
*/
|
||||
private void updateMediaNotification() {
|
||||
BackgroundService.RunCommand(context, service -> {
|
||||
//If the user disabled the media notification, do not show it
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (!prefs.getBoolean(context.getString(R.string.mpris_notification_key), true)) {
|
||||
closeMediaNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
//Make sure our information is up-to-date
|
||||
updateCurrentPlayer(service);
|
||||
//If the user disabled the media notification, do not show it
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (!prefs.getBoolean(context.getString(R.string.mpris_notification_key), true)) {
|
||||
closeMediaNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
//If the player disappeared (and no other playing one found), just remove the notification
|
||||
if (notificationPlayer == null) {
|
||||
closeMediaNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
//Update the metadata and playback status
|
||||
synchronized (instance) {
|
||||
if (mediaSession == null) {
|
||||
mediaSession = new MediaSessionCompat(context, MPRIS_MEDIA_SESSION_TAG);
|
||||
mediaSession.setCallback(mediaSessionCallback);
|
||||
mediaSession.setCallback(mediaSessionCallback, new Handler(context.getMainLooper()));
|
||||
// Deprecated flags not required in Build.VERSION_CODES.O and later
|
||||
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
|
||||
}
|
||||
}
|
||||
|
||||
updateRemoteDeviceVolumeControl();
|
||||
//Make sure our information is up-to-date
|
||||
updateCurrentPlayer();
|
||||
|
||||
MediaMetadataCompat.Builder metadata = new MediaMetadataCompat.Builder();
|
||||
//If the player disappeared (and no other playing one found), just remove the notification
|
||||
if (notificationPlayer == null) {
|
||||
closeMediaNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
//Fallback because older KDE connect versions do not support getTitle()
|
||||
if (!notificationPlayer.getTitle().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getTitle());
|
||||
} else {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getCurrentSong());
|
||||
}
|
||||
if (!notificationPlayer.getArtist().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, notificationPlayer.getArtist());
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, notificationPlayer.getArtist());
|
||||
}
|
||||
if (!notificationPlayer.getAlbum().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, notificationPlayer.getAlbum());
|
||||
}
|
||||
if (notificationPlayer.getLength() > 0) {
|
||||
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength());
|
||||
updateRemoteDeviceVolumeControl();
|
||||
|
||||
MediaMetadataCompat.Builder metadata = new MediaMetadataCompat.Builder();
|
||||
|
||||
//Fallback because older KDE connect versions do not support getTitle()
|
||||
if (!notificationPlayer.getTitle().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getTitle());
|
||||
} else {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getCurrentSong());
|
||||
}
|
||||
if (!notificationPlayer.getArtist().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, notificationPlayer.getArtist());
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, notificationPlayer.getArtist());
|
||||
}
|
||||
if (!notificationPlayer.getAlbum().isEmpty()) {
|
||||
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, notificationPlayer.getAlbum());
|
||||
}
|
||||
if (notificationPlayer.getLength() > 0) {
|
||||
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength());
|
||||
}
|
||||
|
||||
Bitmap albumArt = notificationPlayer.getAlbumArt();
|
||||
if (albumArt != null) {
|
||||
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt);
|
||||
}
|
||||
|
||||
mediaSession.setMetadata(metadata.build());
|
||||
PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder();
|
||||
|
||||
if (notificationPlayer.isPlaying()) {
|
||||
playbackState.setState(PlaybackStateCompat.STATE_PLAYING, notificationPlayer.getPosition(), 1.0f);
|
||||
} else {
|
||||
playbackState.setState(PlaybackStateCompat.STATE_PAUSED, notificationPlayer.getPosition(), 0.0f);
|
||||
}
|
||||
|
||||
//Create all actions (previous/play/pause/next)
|
||||
Intent iPlay = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iPlay.setAction(MprisMediaNotificationReceiver.ACTION_PLAY);
|
||||
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
PendingIntent piPlay = PendingIntent.getBroadcast(context, 0, iPlay, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aPlay = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_play_white, context.getString(R.string.mpris_play), piPlay);
|
||||
|
||||
Intent iPause = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iPause.setAction(MprisMediaNotificationReceiver.ACTION_PAUSE);
|
||||
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
PendingIntent piPause = PendingIntent.getBroadcast(context, 0, iPause, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aPause = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_pause_white, context.getString(R.string.mpris_pause), piPause);
|
||||
|
||||
Intent iPrevious = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iPrevious.setAction(MprisMediaNotificationReceiver.ACTION_PREVIOUS);
|
||||
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
PendingIntent piPrevious = PendingIntent.getBroadcast(context, 0, iPrevious, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aPrevious = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_previous_white, context.getString(R.string.mpris_previous), piPrevious);
|
||||
|
||||
Intent iNext = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iNext.setAction(MprisMediaNotificationReceiver.ACTION_NEXT);
|
||||
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
PendingIntent piNext = PendingIntent.getBroadcast(context, 0, iNext, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aNext = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_next_white, context.getString(R.string.mpris_next), piNext);
|
||||
|
||||
Intent iOpenActivity = new Intent(context, MprisActivity.class);
|
||||
iOpenActivity.putExtra("deviceId", notificationDevice);
|
||||
iOpenActivity.putExtra("player", notificationPlayer.getPlayerName());
|
||||
|
||||
PendingIntent piOpenActivity = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(iOpenActivity)
|
||||
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
|
||||
NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NotificationHelper.Channels.MEDIA_CONTROL);
|
||||
|
||||
notification
|
||||
.setAutoCancel(false)
|
||||
.setContentIntent(piOpenActivity)
|
||||
.setSmallIcon(R.drawable.ic_play_white)
|
||||
.setShowWhen(false)
|
||||
.setColor(ContextCompat.getColor(context, R.color.primary))
|
||||
.setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setSubText(KdeConnect.getInstance().getDevice(notificationDevice).getName());
|
||||
|
||||
if (!notificationPlayer.getTitle().isEmpty()) {
|
||||
notification.setContentTitle(notificationPlayer.getTitle());
|
||||
} else {
|
||||
notification.setContentTitle(notificationPlayer.getCurrentSong());
|
||||
}
|
||||
//Only set the notification body text if we have an author and/or album
|
||||
if (!notificationPlayer.getArtist().isEmpty() && !notificationPlayer.getAlbum().isEmpty()) {
|
||||
notification.setContentText(notificationPlayer.getArtist() + " - " + notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayerName() + ")");
|
||||
} else if (!notificationPlayer.getArtist().isEmpty()) {
|
||||
notification.setContentText(notificationPlayer.getArtist() + " (" + notificationPlayer.getPlayerName() + ")");
|
||||
} else if (!notificationPlayer.getAlbum().isEmpty()) {
|
||||
notification.setContentText(notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayerName() + ")");
|
||||
} else {
|
||||
notification.setContentText(notificationPlayer.getPlayerName());
|
||||
}
|
||||
|
||||
if (albumArt != null) {
|
||||
notification.setLargeIcon(albumArt);
|
||||
}
|
||||
|
||||
if (!notificationPlayer.isPlaying()) {
|
||||
Intent iCloseNotification = new Intent(context, MprisMediaNotificationReceiver.class);
|
||||
iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION);
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
|
||||
PendingIntent piCloseNotification = PendingIntent.getBroadcast(context, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
notification.setDeleteIntent(piCloseNotification);
|
||||
}
|
||||
|
||||
//Add media control actions
|
||||
int numActions = 0;
|
||||
long playbackActions = 0;
|
||||
if (notificationPlayer.isGoPreviousAllowed()) {
|
||||
notification.addAction(aPrevious.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
||||
++numActions;
|
||||
}
|
||||
if (notificationPlayer.isPlaying() && notificationPlayer.isPauseAllowed()) {
|
||||
notification.addAction(aPause.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
|
||||
++numActions;
|
||||
}
|
||||
if (!notificationPlayer.isPlaying() && notificationPlayer.isPlayAllowed()) {
|
||||
notification.addAction(aPlay.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_PLAY;
|
||||
++numActions;
|
||||
}
|
||||
if (notificationPlayer.isGoNextAllowed()) {
|
||||
notification.addAction(aNext.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
|
||||
++numActions;
|
||||
}
|
||||
// Documentation says that this was added in Lollipop (21) but it seems to cause crashes on < Pie (28)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
if (notificationPlayer.isSeekAllowed()) {
|
||||
playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
|
||||
}
|
||||
}
|
||||
playbackState.setActions(playbackActions);
|
||||
mediaSession.setPlaybackState(playbackState.build());
|
||||
|
||||
Bitmap albumArt = notificationPlayer.getAlbumArt();
|
||||
if (albumArt != null) {
|
||||
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt);
|
||||
}
|
||||
//Only allow deletion if no music is notificationPlayer
|
||||
notification.setOngoing(notificationPlayer.isPlaying());
|
||||
|
||||
mediaSession.setMetadata(metadata.build());
|
||||
PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder();
|
||||
//Use the MediaStyle notification, so it feels like other media players. That also allows adding actions
|
||||
MediaStyle mediaStyle = new MediaStyle();
|
||||
if (numActions == 1) {
|
||||
mediaStyle.setShowActionsInCompactView(0);
|
||||
} else if (numActions == 2) {
|
||||
mediaStyle.setShowActionsInCompactView(0, 1);
|
||||
} else if (numActions >= 3) {
|
||||
mediaStyle.setShowActionsInCompactView(0, 1, 2);
|
||||
}
|
||||
mediaStyle.setMediaSession(mediaSession.getSessionToken());
|
||||
notification.setStyle(mediaStyle);
|
||||
notification.setGroup("MprisMediaSession");
|
||||
|
||||
if (notificationPlayer.isPlaying()) {
|
||||
playbackState.setState(PlaybackStateCompat.STATE_PLAYING, notificationPlayer.getPosition(), 1.0f);
|
||||
} else {
|
||||
playbackState.setState(PlaybackStateCompat.STATE_PAUSED, notificationPlayer.getPosition(), 0.0f);
|
||||
}
|
||||
|
||||
//Create all actions (previous/play/pause/next)
|
||||
Intent iPlay = new Intent(service, MprisMediaNotificationReceiver.class);
|
||||
iPlay.setAction(MprisMediaNotificationReceiver.ACTION_PLAY);
|
||||
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
|
||||
PendingIntent piPlay = PendingIntent.getBroadcast(service, 0, iPlay, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aPlay = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_play_white, service.getString(R.string.mpris_play), piPlay);
|
||||
|
||||
Intent iPause = new Intent(service, MprisMediaNotificationReceiver.class);
|
||||
iPause.setAction(MprisMediaNotificationReceiver.ACTION_PAUSE);
|
||||
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
|
||||
PendingIntent piPause = PendingIntent.getBroadcast(service, 0, iPause, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aPause = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_pause_white, service.getString(R.string.mpris_pause), piPause);
|
||||
|
||||
Intent iPrevious = new Intent(service, MprisMediaNotificationReceiver.class);
|
||||
iPrevious.setAction(MprisMediaNotificationReceiver.ACTION_PREVIOUS);
|
||||
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
|
||||
PendingIntent piPrevious = PendingIntent.getBroadcast(service, 0, iPrevious, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aPrevious = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_previous_white, service.getString(R.string.mpris_previous), piPrevious);
|
||||
|
||||
Intent iNext = new Intent(service, MprisMediaNotificationReceiver.class);
|
||||
iNext.setAction(MprisMediaNotificationReceiver.ACTION_NEXT);
|
||||
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
|
||||
PendingIntent piNext = PendingIntent.getBroadcast(service, 0, iNext, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
NotificationCompat.Action.Builder aNext = new NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_next_white, service.getString(R.string.mpris_next), piNext);
|
||||
|
||||
Intent iOpenActivity = new Intent(service, MprisActivity.class);
|
||||
iOpenActivity.putExtra("deviceId", notificationDevice);
|
||||
iOpenActivity.putExtra("player", notificationPlayer.getPlayer());
|
||||
|
||||
PendingIntent piOpenActivity = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(iOpenActivity)
|
||||
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
|
||||
NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NotificationHelper.Channels.MEDIA_CONTROL);
|
||||
|
||||
notification
|
||||
.setAutoCancel(false)
|
||||
.setContentIntent(piOpenActivity)
|
||||
.setSmallIcon(R.drawable.ic_play_white)
|
||||
.setShowWhen(false)
|
||||
.setColor(ContextCompat.getColor(service, R.color.primary))
|
||||
.setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setSubText(service.getDevice(notificationDevice).getName());
|
||||
|
||||
if (!notificationPlayer.getTitle().isEmpty()) {
|
||||
notification.setContentTitle(notificationPlayer.getTitle());
|
||||
} else {
|
||||
notification.setContentTitle(notificationPlayer.getCurrentSong());
|
||||
}
|
||||
//Only set the notification body text if we have an author and/or album
|
||||
if (!notificationPlayer.getArtist().isEmpty() && !notificationPlayer.getAlbum().isEmpty()) {
|
||||
notification.setContentText(notificationPlayer.getArtist() + " - " + notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayer() + ")");
|
||||
} else if (!notificationPlayer.getArtist().isEmpty()) {
|
||||
notification.setContentText(notificationPlayer.getArtist() + " (" + notificationPlayer.getPlayer() + ")");
|
||||
} else if (!notificationPlayer.getAlbum().isEmpty()) {
|
||||
notification.setContentText(notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayer() + ")");
|
||||
} else {
|
||||
notification.setContentText(notificationPlayer.getPlayer());
|
||||
}
|
||||
|
||||
if (albumArt != null) {
|
||||
notification.setLargeIcon(albumArt);
|
||||
}
|
||||
|
||||
if (!notificationPlayer.isPlaying()) {
|
||||
Intent iCloseNotification = new Intent(service, MprisMediaNotificationReceiver.class);
|
||||
iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION);
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
|
||||
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
|
||||
PendingIntent piCloseNotification = PendingIntent.getBroadcast(service, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
notification.setDeleteIntent(piCloseNotification);
|
||||
}
|
||||
|
||||
//Add media control actions
|
||||
int numActions = 0;
|
||||
long playbackActions = 0;
|
||||
if (notificationPlayer.isGoPreviousAllowed()) {
|
||||
notification.addAction(aPrevious.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
||||
++numActions;
|
||||
}
|
||||
if (notificationPlayer.isPlaying() && notificationPlayer.isPauseAllowed()) {
|
||||
notification.addAction(aPause.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
|
||||
++numActions;
|
||||
}
|
||||
if (!notificationPlayer.isPlaying() && notificationPlayer.isPlayAllowed()) {
|
||||
notification.addAction(aPlay.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_PLAY;
|
||||
++numActions;
|
||||
}
|
||||
if (notificationPlayer.isGoNextAllowed()) {
|
||||
notification.addAction(aNext.build());
|
||||
playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
|
||||
++numActions;
|
||||
}
|
||||
// Documentation says that this was added in Lollipop (21) but it seems to cause crashes on < Pie (28)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
if (notificationPlayer.isSeekAllowed()) {
|
||||
playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
|
||||
}
|
||||
}
|
||||
playbackState.setActions(playbackActions);
|
||||
mediaSession.setPlaybackState(playbackState.build());
|
||||
|
||||
//Only allow deletion if no music is notificationPlayer
|
||||
notification.setOngoing(notificationPlayer.isPlaying());
|
||||
|
||||
//Use the MediaStyle notification, so it feels like other media players. That also allows adding actions
|
||||
MediaStyle mediaStyle = new MediaStyle();
|
||||
if (numActions == 1) {
|
||||
mediaStyle.setShowActionsInCompactView(0);
|
||||
} else if (numActions == 2) {
|
||||
mediaStyle.setShowActionsInCompactView(0, 1);
|
||||
} else if (numActions >= 3) {
|
||||
mediaStyle.setShowActionsInCompactView(0, 1, 2);
|
||||
}
|
||||
mediaStyle.setMediaSession(mediaSession.getSessionToken());
|
||||
notification.setStyle(mediaStyle);
|
||||
notification.setGroup("MprisMediaSession");
|
||||
|
||||
//Display the notification
|
||||
mediaSession.setActive(true);
|
||||
final NotificationManager nm = ContextCompat.getSystemService(context, NotificationManager.class);
|
||||
nm.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build());
|
||||
});
|
||||
//Display the notification
|
||||
mediaSession.setActive(true);
|
||||
final NotificationManager nm = ContextCompat.getSystemService(context, NotificationManager.class);
|
||||
nm.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build());
|
||||
}
|
||||
|
||||
public void closeMediaNotification() {
|
||||
@@ -459,16 +456,18 @@ public class MprisMediaSession implements
|
||||
|
||||
//Clear the current player and media session
|
||||
notificationPlayer = null;
|
||||
if (mediaSession != null) {
|
||||
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().build());
|
||||
mediaSession.setMetadata(new MediaMetadataCompat.Builder().build());
|
||||
mediaSession.setActive(false);
|
||||
mediaSession.release();
|
||||
mediaSession = null;
|
||||
synchronized (instance) {
|
||||
if (mediaSession != null) {
|
||||
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().build());
|
||||
mediaSession.setMetadata(new MediaMetadataCompat.Builder().build());
|
||||
mediaSession.setActive(false);
|
||||
mediaSession.release();
|
||||
mediaSession = null;
|
||||
|
||||
SystemVolumeProvider currentProvider = SystemVolumeProvider.getCurrentProvider();
|
||||
if (currentProvider != null) {
|
||||
currentProvider.release();
|
||||
SystemVolumeProvider currentProvider = SystemVolumeProvider.getCurrentProvider();
|
||||
if (currentProvider != null) {
|
||||
currentProvider.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
@@ -10,7 +9,6 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -28,12 +26,9 @@ import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Helpers.VideoUrlsHelper;
|
||||
import org.kde.kdeconnect.Helpers.VolumeHelperKt;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.KdeConnect;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.MprisControlBinding;
|
||||
import org.kde.kdeconnect_tp.databinding.MprisNowPlayingBinding;
|
||||
@@ -49,18 +44,9 @@ public class MprisNowPlayingFragment extends Fragment implements VolumeKeyListen
|
||||
private MprisNowPlayingBinding activityMprisBinding;
|
||||
private String deviceId;
|
||||
private Runnable positionSeekUpdateRunnable = null;
|
||||
|
||||
private String targetPlayerName = "";
|
||||
private MprisPlugin.MprisPlayer targetPlayer = null;
|
||||
private final BaseLinkProvider.ConnectionReceiver connectionReceiver = new BaseLinkProvider.ConnectionReceiver() {
|
||||
@Override
|
||||
public void onConnectionReceived(NetworkPacket identityPacket, BaseLink link) {
|
||||
connectToPlugin(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionLost(BaseLink link) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
public static MprisNowPlayingFragment newInstance(String deviceId) {
|
||||
MprisNowPlayingFragment mprisNowPlayingFragment = new MprisNowPlayingFragment();
|
||||
@@ -94,211 +80,217 @@ public class MprisNowPlayingFragment extends Fragment implements VolumeKeyListen
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
activityMprisBinding = MprisNowPlayingBinding.inflate(inflater);
|
||||
mprisControlBinding = activityMprisBinding.mprisControl;
|
||||
|
||||
if (activityMprisBinding == null) {
|
||||
activityMprisBinding = MprisNowPlayingBinding.inflate(inflater);
|
||||
mprisControlBinding = activityMprisBinding.mprisControl;
|
||||
deviceId = requireArguments().getString(MprisPlugin.DEVICE_ID_KEY);
|
||||
|
||||
String targetPlayerName = "";
|
||||
Intent activityIntent = requireActivity().getIntent();
|
||||
activityIntent.getStringExtra("player");
|
||||
targetPlayerName = "";
|
||||
Intent activityIntent = requireActivity().getIntent();
|
||||
if (activityIntent.hasExtra("player")) {
|
||||
targetPlayerName = activityIntent.getStringExtra("player");
|
||||
activityIntent.removeExtra("player");
|
||||
} else if (savedInstanceState != null) {
|
||||
targetPlayerName = savedInstanceState.getString("targetPlayer");
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(targetPlayerName)) {
|
||||
if (savedInstanceState != null) {
|
||||
targetPlayerName = savedInstanceState.getString("targetPlayer");
|
||||
}
|
||||
connectToPlugin();
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
String interval_time_str = prefs.getString(getString(R.string.mpris_time_key),
|
||||
getString(R.string.mpris_time_default));
|
||||
final int interval_time = Integer.parseInt(interval_time_str);
|
||||
|
||||
performActionOnClick(mprisControlBinding.loopButton, p -> {
|
||||
switch (p.getLoopStatus()) {
|
||||
case "None":
|
||||
p.setLoopStatus("Track");
|
||||
break;
|
||||
case "Track":
|
||||
p.setLoopStatus("Playlist");
|
||||
break;
|
||||
case "Playlist":
|
||||
p.setLoopStatus("None");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
performActionOnClick(mprisControlBinding.playButton, MprisPlugin.MprisPlayer::playPause);
|
||||
|
||||
performActionOnClick(mprisControlBinding.shuffleButton, p -> p.setShuffle(!p.getShuffle()));
|
||||
|
||||
performActionOnClick(mprisControlBinding.prevButton, MprisPlugin.MprisPlayer::previous);
|
||||
|
||||
performActionOnClick(mprisControlBinding.rewButton, p -> targetPlayer.seek(interval_time * -1));
|
||||
|
||||
performActionOnClick(mprisControlBinding.ffButton, p -> p.seek(interval_time));
|
||||
|
||||
performActionOnClick(mprisControlBinding.nextButton, MprisPlugin.MprisPlayer::next);
|
||||
|
||||
performActionOnClick(mprisControlBinding.stopButton, MprisPlugin.MprisPlayer::stop);
|
||||
|
||||
mprisControlBinding.volumeSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
|
||||
}
|
||||
|
||||
deviceId = requireArguments().getString(MprisPlugin.DEVICE_ID_KEY);
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
String interval_time_str = prefs.getString(getString(R.string.mpris_time_key),
|
||||
getString(R.string.mpris_time_default));
|
||||
final int interval_time = Integer.parseInt(interval_time_str);
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
if (targetPlayer == null) return;
|
||||
targetPlayer.setVolume(seekBar.getProgress());
|
||||
}
|
||||
});
|
||||
|
||||
BackgroundService.RunCommand(requireContext(), service -> service.addConnectionListener(connectionReceiver));
|
||||
connectToPlugin(targetPlayerName);
|
||||
positionSeekUpdateRunnable = () -> {
|
||||
if (!isAdded()) return; // Fragment was already detached
|
||||
if (targetPlayer != null) {
|
||||
mprisControlBinding.positionSeek.setProgress((int) (targetPlayer.getPosition()));
|
||||
}
|
||||
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000);
|
||||
};
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
|
||||
|
||||
performActionOnClick(mprisControlBinding.loopButton, p -> {
|
||||
switch (p.getLoopStatus()) {
|
||||
case "None":
|
||||
p.setLoopStatus("Track");
|
||||
break;
|
||||
case "Track":
|
||||
p.setLoopStatus("Playlist");
|
||||
break;
|
||||
case "Playlist":
|
||||
p.setLoopStatus("None");
|
||||
break;
|
||||
mprisControlBinding.positionSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean byUser) {
|
||||
mprisControlBinding.progressTextview.setText(milisToProgress(progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
if (targetPlayer != null) {
|
||||
targetPlayer.setPosition(seekBar.getProgress());
|
||||
}
|
||||
});
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
|
||||
}
|
||||
});
|
||||
|
||||
performActionOnClick(mprisControlBinding.playButton, MprisPlugin.MprisPlayer::playPause);
|
||||
|
||||
performActionOnClick(mprisControlBinding.shuffleButton, p -> p.setShuffle(!p.getShuffle()));
|
||||
|
||||
performActionOnClick(mprisControlBinding.prevButton, MprisPlugin.MprisPlayer::previous);
|
||||
|
||||
performActionOnClick(mprisControlBinding.rewButton, p -> targetPlayer.seek(interval_time * -1));
|
||||
|
||||
performActionOnClick(mprisControlBinding.ffButton, p -> p.seek(interval_time));
|
||||
|
||||
performActionOnClick(mprisControlBinding.nextButton, MprisPlugin.MprisPlayer::next);
|
||||
|
||||
performActionOnClick(mprisControlBinding.stopButton, MprisPlugin.MprisPlayer::stop);
|
||||
|
||||
mprisControlBinding.volumeSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
BackgroundService.RunCommand(requireContext(), service -> {
|
||||
if (targetPlayer == null) return;
|
||||
targetPlayer.setVolume(seekBar.getProgress());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
positionSeekUpdateRunnable = () -> {
|
||||
Context context = getContext();
|
||||
if (context == null) return; // Fragment was already detached
|
||||
BackgroundService.RunCommand(context, service -> {
|
||||
if (targetPlayer != null) {
|
||||
mprisControlBinding.positionSeek.setProgress((int) (targetPlayer.getPosition()));
|
||||
}
|
||||
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000);
|
||||
});
|
||||
};
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
|
||||
|
||||
mprisControlBinding.positionSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean byUser) {
|
||||
mprisControlBinding.progressTextview.setText(milisToProgress(progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
BackgroundService.RunCommand(requireContext(), service -> {
|
||||
if (targetPlayer != null) {
|
||||
targetPlayer.setPosition(seekBar.getProgress());
|
||||
}
|
||||
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mprisControlBinding.nowPlayingTextview.setSelected(true);
|
||||
|
||||
}
|
||||
mprisControlBinding.nowPlayingTextview.setSelected(true);
|
||||
|
||||
return activityMprisBinding.getRoot();
|
||||
}
|
||||
|
||||
private void connectToPlugin(final String targetPlayerName) {
|
||||
BackgroundService.RunWithPlugin(requireContext(), deviceId, MprisPlugin.class, mpris -> {
|
||||
targetPlayer = mpris.getPlayerStatus(targetPlayerName);
|
||||
|
||||
mpris.setPlayerStatusUpdatedHandler("activity", () -> requireActivity().runOnUiThread(() -> updatePlayerStatus(mpris)));
|
||||
mpris.setPlayerListUpdatedHandler("activity", () -> {
|
||||
final List<String> playerList = mpris.getPlayerList();
|
||||
final ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item,
|
||||
playerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY)
|
||||
);
|
||||
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
mprisControlBinding.playerSpinner.setAdapter(adapter);
|
||||
|
||||
if (playerList.isEmpty()) {
|
||||
mprisControlBinding.noPlayers.setVisibility(View.VISIBLE);
|
||||
mprisControlBinding.playerSpinner.setVisibility(View.GONE);
|
||||
mprisControlBinding.nowPlayingTextview.setText("");
|
||||
} else {
|
||||
mprisControlBinding.noPlayers.setVisibility(View.GONE);
|
||||
mprisControlBinding.playerSpinner.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
mprisControlBinding.playerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> arg0, View arg1, int pos, long id) {
|
||||
|
||||
if (pos >= playerList.size()) return;
|
||||
|
||||
String player = playerList.get(pos);
|
||||
if (targetPlayer != null && player.equals(targetPlayer.getPlayer())) {
|
||||
return; //Player hasn't actually changed
|
||||
}
|
||||
targetPlayer = mpris.getPlayerStatus(player);
|
||||
updatePlayerStatus(mpris);
|
||||
|
||||
if (targetPlayer != null && targetPlayer.isPlaying()) {
|
||||
MprisMediaSession.getInstance().playerSelected(targetPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> arg0) {
|
||||
targetPlayer = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (targetPlayer == null) {
|
||||
//If no player is selected, try to select a playing player
|
||||
targetPlayer = mpris.getPlayingPlayer();
|
||||
}
|
||||
//Try to select the specified player
|
||||
if (targetPlayer != null) {
|
||||
int targetIndex = adapter.getPosition(targetPlayer.getPlayer());
|
||||
if (targetIndex >= 0) {
|
||||
mprisControlBinding.playerSpinner.setSelection(targetIndex);
|
||||
} else {
|
||||
targetPlayer = null;
|
||||
}
|
||||
}
|
||||
//If no player selected, select the first one (if any)
|
||||
if (targetPlayer == null && !playerList.isEmpty()) {
|
||||
targetPlayer = mpris.getPlayerStatus(playerList.get(0));
|
||||
mprisControlBinding.playerSpinner.setSelection(0);
|
||||
}
|
||||
updatePlayerStatus(mpris);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
BackgroundService.RunCommand(requireContext(), service -> service.removeConnectionListener(connectionReceiver));
|
||||
public void onDestroyView() {
|
||||
disconnectFromPlugin();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void performActionOnClick(View v, MprisPlayerCallback l) {
|
||||
v.setOnClickListener(view -> BackgroundService.RunCommand(requireContext(), service -> {
|
||||
if (targetPlayer == null) return;
|
||||
l.performAction(targetPlayer);
|
||||
private void disconnectFromPlugin() {
|
||||
MprisPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MprisPlugin.class);
|
||||
if (plugin != null) {
|
||||
plugin.removePlayerListUpdatedHandler("activity");
|
||||
plugin.removePlayerStatusUpdatedHandler("activity");
|
||||
}
|
||||
}
|
||||
|
||||
private void connectToPlugin() {
|
||||
MprisPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MprisPlugin.class);
|
||||
if (plugin == null) {
|
||||
if (isAdded()) {
|
||||
requireActivity().finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
targetPlayer = plugin.getPlayerStatus(targetPlayerName);
|
||||
|
||||
plugin.setPlayerStatusUpdatedHandler("activity", () -> requireActivity().runOnUiThread(() -> {
|
||||
updatePlayerStatus(plugin);
|
||||
}));
|
||||
plugin.setPlayerListUpdatedHandler("activity", () -> requireActivity().runOnUiThread(() -> {
|
||||
final List<String> playerList = plugin.getPlayerList();
|
||||
final ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(),
|
||||
android.R.layout.simple_spinner_item,
|
||||
playerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY)
|
||||
);
|
||||
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
mprisControlBinding.playerSpinner.setAdapter(adapter);
|
||||
|
||||
if (playerList.isEmpty()) {
|
||||
mprisControlBinding.noPlayers.setVisibility(View.VISIBLE);
|
||||
mprisControlBinding.playerSpinner.setVisibility(View.GONE);
|
||||
mprisControlBinding.nowPlayingTextview.setText("");
|
||||
} else {
|
||||
mprisControlBinding.noPlayers.setVisibility(View.GONE);
|
||||
mprisControlBinding.playerSpinner.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
mprisControlBinding.playerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> arg0, View arg1, int pos, long id) {
|
||||
|
||||
if (pos >= playerList.size()) return;
|
||||
|
||||
String player = playerList.get(pos);
|
||||
if (targetPlayer != null && player.equals(targetPlayer.getPlayerName())) {
|
||||
return; //Player hasn't actually changed
|
||||
}
|
||||
targetPlayer = plugin.getPlayerStatus(player);
|
||||
targetPlayerName = targetPlayer.getPlayerName();
|
||||
updatePlayerStatus(plugin);
|
||||
|
||||
if (targetPlayer != null && targetPlayer.isPlaying()) {
|
||||
MprisMediaSession.getInstance().playerSelected(targetPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> arg0) {
|
||||
targetPlayer = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (targetPlayer == null) {
|
||||
//If no player is selected, try to select a playing player
|
||||
targetPlayer = plugin.getPlayingPlayer();
|
||||
}
|
||||
//Try to select the specified player
|
||||
if (targetPlayer != null) {
|
||||
int targetIndex = adapter.getPosition(targetPlayer.getPlayerName());
|
||||
if (targetIndex >= 0) {
|
||||
mprisControlBinding.playerSpinner.setSelection(targetIndex);
|
||||
} else {
|
||||
targetPlayer = null;
|
||||
}
|
||||
}
|
||||
//If no player selected, select the first one (if any)
|
||||
if (targetPlayer == null && !playerList.isEmpty()) {
|
||||
targetPlayer = plugin.getPlayerStatus(playerList.get(0));
|
||||
mprisControlBinding.playerSpinner.setSelection(0);
|
||||
}
|
||||
updatePlayerStatus(plugin);
|
||||
}));
|
||||
}
|
||||
|
||||
private void updatePlayerStatus(MprisPlugin mpris) {
|
||||
private void performActionOnClick(View v, MprisPlayerCallback l) {
|
||||
v.setOnClickListener(view -> {
|
||||
if (targetPlayer == null) return;
|
||||
l.performAction(targetPlayer);
|
||||
});
|
||||
}
|
||||
|
||||
private void updatePlayerStatus(MprisPlugin plugin) {
|
||||
if (!isAdded()) {
|
||||
//Fragment is not attached to an activity. We will crash if we try to do anything here.
|
||||
return;
|
||||
}
|
||||
|
||||
MprisPlugin.MprisPlayer playerStatus = targetPlayer;
|
||||
if (playerStatus == null) {
|
||||
//No player with that name found, just display "empty" data
|
||||
playerStatus = mpris.getEmptyPlayer();
|
||||
playerStatus = plugin.getEmptyPlayer();
|
||||
}
|
||||
String song = playerStatus.getCurrentSong();
|
||||
|
||||
@@ -433,7 +425,7 @@ public class MprisNowPlayingFragment extends Fragment implements VolumeKeyListen
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
if (targetPlayer != null) {
|
||||
outState.putString("targetPlayer", targetPlayer.getPlayer());
|
||||
outState.putString("targetPlayer", targetPlayerName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,10 +11,9 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
@@ -77,12 +76,12 @@ public class MprisPlugin extends Plugin {
|
||||
return album;
|
||||
}
|
||||
|
||||
public String getPlayer() {
|
||||
public String getPlayerName() {
|
||||
return player;
|
||||
}
|
||||
|
||||
boolean isSpotify() {
|
||||
return getPlayer().equalsIgnoreCase("spotify");
|
||||
return getPlayerName().equalsIgnoreCase("spotify");
|
||||
}
|
||||
|
||||
public String getLoopStatus() {
|
||||
@@ -165,55 +164,55 @@ public class MprisPlugin extends Plugin {
|
||||
|
||||
public void playPause() {
|
||||
if (isPauseAllowed() || isPlayAllowed()) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "action", "PlayPause");
|
||||
sendCommand(getPlayerName(), "action", "PlayPause");
|
||||
}
|
||||
}
|
||||
|
||||
public void play() {
|
||||
if (isPlayAllowed()) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "action", "Play");
|
||||
sendCommand(getPlayerName(), "action", "Play");
|
||||
}
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
if (isPauseAllowed()) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "action", "Pause");
|
||||
sendCommand(getPlayerName(), "action", "Pause");
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "action", "Stop");
|
||||
sendCommand(getPlayerName(), "action", "Stop");
|
||||
}
|
||||
|
||||
public void previous() {
|
||||
if (isGoPreviousAllowed()) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "action", "Previous");
|
||||
sendCommand(getPlayerName(), "action", "Previous");
|
||||
}
|
||||
}
|
||||
|
||||
public void next() {
|
||||
if (isGoNextAllowed()) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "action", "Next");
|
||||
sendCommand(getPlayerName(), "action", "Next");
|
||||
}
|
||||
}
|
||||
|
||||
public void setLoopStatus(String loopStatus) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "setLoopStatus", loopStatus);
|
||||
sendCommand(getPlayerName(), "setLoopStatus", loopStatus);
|
||||
}
|
||||
|
||||
public void setShuffle(boolean shuffle) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "setShuffle", shuffle);
|
||||
sendCommand(getPlayerName(), "setShuffle", shuffle);
|
||||
}
|
||||
|
||||
public void setVolume(int volume) {
|
||||
if (isSetVolumeAllowed()) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "setVolume", volume);
|
||||
sendCommand(getPlayerName(), "setVolume", volume);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
if (isSeekAllowed()) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "SetPosition", position);
|
||||
sendCommand(getPlayerName(), "SetPosition", position);
|
||||
|
||||
lastPosition = position;
|
||||
lastPositionTime = System.currentTimeMillis();
|
||||
@@ -222,7 +221,7 @@ public class MprisPlugin extends Plugin {
|
||||
|
||||
public void seek(int offset) {
|
||||
if (isSeekAllowed()) {
|
||||
MprisPlugin.this.sendCommand(getPlayer(), "Seek", offset);
|
||||
sendCommand(getPlayerName(), "Seek", offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,12 +240,12 @@ public class MprisPlugin extends Plugin {
|
||||
private final ConcurrentHashMap<String, Callback> playerListUpdated = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getResources().getString(R.string.pref_plugin_mpris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getResources().getString(R.string.pref_plugin_mpris_desc);
|
||||
}
|
||||
|
||||
@@ -307,7 +306,7 @@ public class MprisPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
if (np.getBoolean("transferringAlbumArt", false)) {
|
||||
AlbumArtCache.payloadToDiskCache(np.getString("albumArtUrl"), np.getPayload());
|
||||
return true;
|
||||
@@ -407,12 +406,12 @@ public class MprisPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MPRIS};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MPRIS_REQUEST};
|
||||
}
|
||||
|
||||
@@ -495,7 +494,7 @@ public class MprisPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActionName() {
|
||||
public @NonNull String getActionName() {
|
||||
return context.getString(R.string.open_mpris_controls);
|
||||
}
|
||||
|
||||
@@ -522,7 +521,7 @@ public class MprisPlugin extends Plugin {
|
||||
|
||||
if (player.albumArtUrl.equals(url)) {
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
|
||||
np.set("player", player.getPlayer());
|
||||
np.set("player", player.getPlayerName());
|
||||
np.set("albumArtUrl", url);
|
||||
device.sendPacket(np);
|
||||
return true;
|
||||
|
@@ -15,6 +15,7 @@ import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -86,17 +87,17 @@ public class MprisReceiverPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
public @NonNull String getDisplayName() {
|
||||
return context.getResources().getString(R.string.pref_plugin_mprisreceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
public @NonNull String getDescription() {
|
||||
return context.getResources().getString(R.string.pref_plugin_mprisreceiver_desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPacketReceived(NetworkPacket np) {
|
||||
public boolean onPacketReceived(@NonNull NetworkPacket np) {
|
||||
|
||||
if (np.getBoolean("requestPlayerList")) {
|
||||
sendPlayerList();
|
||||
@@ -158,12 +159,12 @@ public class MprisReceiverPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedPacketTypes() {
|
||||
public @NonNull String[] getSupportedPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MPRIS_REQUEST};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getOutgoingPacketTypes() {
|
||||
public @NonNull String[] getOutgoingPacketTypes() {
|
||||
return new String[]{PACKET_TYPE_MPRIS};
|
||||
}
|
||||
|
||||
@@ -233,7 +234,7 @@ public class MprisReceiverPlugin extends Plugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DialogFragment getPermissionExplanationDialog() {
|
||||
public @NonNull DialogFragment getPermissionExplanationDialog() {
|
||||
return new StartActivityAlertDialogFragment.Builder()
|
||||
.setTitle(R.string.pref_plugin_mpris)
|
||||
.setMessage(R.string.no_permission_mprisreceiver)
|
||||
|
@@ -17,6 +17,8 @@ import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
@@ -27,15 +29,16 @@ import android.widget.ListView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import com.google.android.material.materialswitch.MaterialSwitch;
|
||||
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper;
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivityNotificationFilterBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -54,18 +57,20 @@ public class NotificationFilterActivity extends AppCompatActivity {
|
||||
boolean isEnabled;
|
||||
}
|
||||
|
||||
private AppListInfo[] apps;
|
||||
// This variable stores all app information and serves as a data source for filtering.
|
||||
private List<AppListInfo> mAllApps;
|
||||
private List<AppListInfo> apps; // Filtered data.
|
||||
|
||||
class AppListAdapter extends BaseAdapter {
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return apps.length + 1;
|
||||
return apps.size() + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppListInfo getItem(int position) {
|
||||
return apps[position - 1];
|
||||
return apps.get(position - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,10 +87,13 @@ public class NotificationFilterActivity extends AppCompatActivity {
|
||||
if (position == 0) {
|
||||
checkedTextView.setText(R.string.all);
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(checkedTextView, null, null, null, null);
|
||||
binding.lvFilterApps.setItemChecked(position, appDatabase.getAllEnabled());
|
||||
} else {
|
||||
checkedTextView.setText(apps[position - 1].name);
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(checkedTextView, apps[position - 1].icon, null, null, null);
|
||||
final AppListInfo info = apps.get(position - 1);
|
||||
checkedTextView.setText(info.name);
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(checkedTextView, info.icon, null, null, null);
|
||||
checkedTextView.setCompoundDrawablePadding((int) (8 * getResources().getDisplayMetrics().density));
|
||||
binding.lvFilterApps.setItemChecked(position, info.isEnabled);
|
||||
}
|
||||
|
||||
return view;
|
||||
@@ -116,18 +124,18 @@ public class NotificationFilterActivity extends AppCompatActivity {
|
||||
List<ApplicationInfo> appList = packageManager.getInstalledApplications(0);
|
||||
int count = appList.size();
|
||||
|
||||
apps = new AppListInfo[count];
|
||||
AppListInfo[] allApps = new AppListInfo[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
ApplicationInfo appInfo = appList.get(i);
|
||||
apps[i] = new AppListInfo();
|
||||
apps[i].pkg = appInfo.packageName;
|
||||
apps[i].name = appInfo.loadLabel(packageManager).toString();
|
||||
apps[i].icon = resizeIcon(appInfo.loadIcon(packageManager), 48);
|
||||
apps[i].isEnabled = appDatabase.isEnabled(appInfo.packageName);
|
||||
allApps[i] = new AppListInfo();
|
||||
allApps[i].pkg = appInfo.packageName;
|
||||
allApps[i].name = appInfo.loadLabel(packageManager).toString();
|
||||
allApps[i].icon = resizeIcon(appInfo.loadIcon(packageManager), 48);
|
||||
allApps[i].isEnabled = appDatabase.isEnabled(appInfo.packageName);
|
||||
}
|
||||
|
||||
Arrays.sort(apps, (lhs, rhs) -> lhs.name.compareToIgnoreCase(rhs.name));
|
||||
|
||||
Arrays.sort(allApps, (lhs, rhs) -> lhs.name.compareToIgnoreCase(rhs.name));
|
||||
mAllApps = Arrays.asList(allApps);
|
||||
apps = new ArrayList<>(mAllApps);
|
||||
runOnUiThread(this::displayAppList);
|
||||
});
|
||||
|
||||
@@ -152,14 +160,16 @@ public class NotificationFilterActivity extends AppCompatActivity {
|
||||
listView.setOnItemClickListener((adapterView, view, i, l) -> {
|
||||
if (i == 0) {
|
||||
boolean enabled = listView.isItemChecked(0);
|
||||
for (int j = 0; j < apps.length; j++) {
|
||||
listView.setItemChecked(j, enabled);
|
||||
for (int j = 0; j < mAllApps.size(); j++) {
|
||||
mAllApps.get(j).isEnabled = enabled;
|
||||
}
|
||||
appDatabase.setAllEnabled(enabled);
|
||||
((AppListAdapter) adapterView.getAdapter()).notifyDataSetChanged();
|
||||
} else {
|
||||
boolean checked = listView.isItemChecked(i);
|
||||
appDatabase.setEnabled(apps[i - 1].pkg, checked);
|
||||
apps[i - 1].isEnabled = checked;
|
||||
apps.get(i - 1).isEnabled = checked;
|
||||
appDatabase.setEnabled(apps.get(i - 1).pkg, checked);
|
||||
((AppListAdapter) adapterView.getAdapter()).notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
listView.setOnItemLongClickListener((adapterView, view, i, l) -> {
|
||||
@@ -185,7 +195,7 @@ public class NotificationFilterActivity extends AppCompatActivity {
|
||||
switch (new_i){
|
||||
case 0:
|
||||
AlertDialog.Builder myBuilder = new AlertDialog.Builder(context);
|
||||
String packageName = apps[i - 1].pkg;
|
||||
String packageName = apps.get(i - 1).pkg;
|
||||
|
||||
View myView = getLayoutInflater().inflate(R.layout.privacy_options, null);
|
||||
CheckBox checkbox_contents = myView.findViewById(R.id.checkbox_contents);
|
||||
@@ -218,8 +228,8 @@ public class NotificationFilterActivity extends AppCompatActivity {
|
||||
});
|
||||
|
||||
listView.setItemChecked(0, appDatabase.getAllEnabled()); //"Select all" button
|
||||
for (int i = 0; i < apps.length; i++) {
|
||||
listView.setItemChecked(i + 1, apps[i].isEnabled);
|
||||
for (int i = 0; i < apps.size(); i++) {
|
||||
listView.setItemChecked(i + 1, apps.get(i).isEnabled);
|
||||
}
|
||||
|
||||
listView.setVisibility(View.VISIBLE);
|
||||
@@ -246,4 +256,40 @@ public class NotificationFilterActivity extends AppCompatActivity {
|
||||
super.onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuItem mitem = menu.add(android.R.string.search_go);
|
||||
mitem.setIcon(R.drawable.ic_search_24);
|
||||
mitem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
SearchView searchView = new SearchView(this);
|
||||
mitem.setActionView(searchView);
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
if(mAllApps == null) return false;
|
||||
apps.clear();
|
||||
if(newText.isEmpty()){
|
||||
apps.addAll(mAllApps);
|
||||
} else {
|
||||
for (AppListInfo s : mAllApps) {
|
||||
if (s.name.toLowerCase().contains(newText.toLowerCase().trim()))
|
||||
apps.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
((AppListAdapter) binding.lvFilterApps.getAdapter()).notifyDataSetChanged();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|