From 75dbac61dafcbc1ab453755fcc4aef78d077c5a7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 1 Nov 2025 15:20:57 +0100 Subject: [PATCH 01/37] chore: bump version to 4.0.0-beta.2 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index 5f88270472..5e1ed47487 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=4.0.0-beta.1 -VERSION_CODE=185 +VERSION_NAME=4.0.0-beta.2 +VERSION_CODE=186 VERSION_NUM=01 \ No newline at end of file From e2950c04dc322562e4507d10d574f6dc584abc16 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 1 Nov 2025 15:21:21 +0100 Subject: [PATCH 02/37] chore: set date in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0376953500..adee47d591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01) -#### TO BE RELEASED +#### 01 November 2025 ## Added From 50bf54039b66610de4e54277cce6f690056cc4cb Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 1 Nov 2025 15:53:30 +0100 Subject: [PATCH 03/37] output libevdev to a custom build directory so it can be linked to in other modules --- sysbridge/src/main/cpp/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sysbridge/src/main/cpp/CMakeLists.txt b/sysbridge/src/main/cpp/CMakeLists.txt index f87842b840..5781ee5879 100644 --- a/sysbridge/src/main/cpp/CMakeLists.txt +++ b/sysbridge/src/main/cpp/CMakeLists.txt @@ -94,6 +94,13 @@ add_library(evdev SHARED android/libbase/stringprintf.cpp ${aidl_src_dir}/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp) +# Set a stable custom output directory for libevdev.so so it can be linked to in other modules +# Outputs to custom "libs" directory in the sysbridge module build directory +set_target_properties(evdev PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../../../build/libs/${ANDROID_ABI}" + LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_SOURCE_DIR}/../../../build/libs/${ANDROID_ABI}" + LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_SOURCE_DIR}/../../../build/libs/${ANDROID_ABI}") + find_library( binder_ndk-lib binder_ndk From b28bedf9e712d9845f0b7705e9888e62e26e30e1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 02:19:10 +0100 Subject: [PATCH 04/37] chore: specify ndk version in a separate file so it can be synced between multiple native modules --- sysbridge/NDK_VERSION | 1 + sysbridge/build.gradle.kts | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 sysbridge/NDK_VERSION diff --git a/sysbridge/NDK_VERSION b/sysbridge/NDK_VERSION new file mode 100644 index 0000000000..b48e72d6d9 --- /dev/null +++ b/sysbridge/NDK_VERSION @@ -0,0 +1 @@ +27.2.12479018 \ No newline at end of file diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts index 758bc153e9..c1bf2d10a2 100644 --- a/sysbridge/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -13,6 +13,16 @@ android { namespace = "io.github.sds100.keymapper.sysbridge" compileSdk = libs.versions.compile.sdk.get().toInt() + // Read NDK version from NDK_VERSION file, with fallback to gradle.properties + // The NDK version is stored in a file so the same value can be used across multiple modules. + val ndkVersionFile = project.file("NDK_VERSION") + val ndkVersionFromFile = if (ndkVersionFile.exists()) { + ndkVersionFile.readText().trim() + } else { + null + } + ndkVersion = ndkVersionFromFile!! + defaultConfig { // Must be API 29 so that the binder-ndk library can be found. minSdk = 29 From 38287956d811d9520fde16ec889655c13d67fcf5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 14:15:17 +0100 Subject: [PATCH 05/37] #1688 fix: use REPLACE instead of ABORT when inserting/updating a key map with a conflicting uid --- .../java/io/github/sds100/keymapper/data/db/dao/KeyMapDao.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/io/github/sds100/keymapper/data/db/dao/KeyMapDao.kt b/data/src/main/java/io/github/sds100/keymapper/data/db/dao/KeyMapDao.kt index 52a213c074..ec692c2871 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/db/dao/KeyMapDao.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/db/dao/KeyMapDao.kt @@ -61,7 +61,7 @@ interface KeyMapDao { @Query("UPDATE $TABLE_NAME SET $KEY_GROUP_UID=(:groupUid) WHERE $KEY_UID in (:uid)") suspend fun setKeyMapGroup(groupUid: String?, vararg uid: String) - @Insert(onConflict = OnConflictStrategy.ABORT) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(vararg keyMap: KeyMapEntity) @Delete @@ -73,7 +73,7 @@ interface KeyMapDao { @Query("DELETE FROM $TABLE_NAME WHERE $KEY_UID in (:uid)") suspend fun deleteById(vararg uid: String) - @Update(onConflict = OnConflictStrategy.ABORT) + @Update(onConflict = OnConflictStrategy.REPLACE) suspend fun update(vararg keyMap: KeyMapEntity) @Query("SELECT COUNT(*) FROM $TABLE_NAME") From fff5f3436e30bc87ec04b9527e216305a1454b58 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 14:29:03 +0100 Subject: [PATCH 06/37] style: add ktlint to system module --- system/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/system/build.gradle.kts b/system/build.gradle.kts index b70b399c4d..dbac603be3 100644 --- a/system/build.gradle.kts +++ b/system/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.dagger.hilt.android) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.jlleitschuh.gradle.ktlint) } android { From 915ed6a50741860594890db5c2b742dce6edf7b4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 14:38:56 +0100 Subject: [PATCH 07/37] style: reformat system module --- .../system/AndroidSystemFeatureAdapter.kt | 7 +- .../keymapper/system/JobSchedulerHelper.kt | 4 +- .../keymapper/system/SystemHiltModule.kt | 12 ++- .../AccessibilityServiceState.kt | 1 - .../AndroidAirplaneModeAdapter.kt | 10 ++- .../keymapper/system/apps/ActivityInfo.kt | 5 +- .../system/apps/AndroidAppShortcutAdapter.kt | 7 +- .../apps/AndroidPackageManagerAdapter.kt | 9 +- .../keymapper/system/apps/AppShortcutInfo.kt | 5 +- .../KeyMapShortcutActivityIntentBuilder.kt | 5 +- .../keymapper/system/apps/PackageInfo.kt | 2 - .../system/apps/PackageManagerAdapter.kt | 1 - .../system/apps/TrampolineActivity.kt | 2 +- .../bluetooth/AndroidBluetoothAdapter.kt | 7 +- .../system/camera/AndroidCameraAdapter.kt | 15 ++-- .../clipboard/AndroidClipboardAdapter.kt | 2 +- .../system/devices/AndroidDevicesAdapter.kt | 8 +- .../system/display/AndroidDisplayAdapter.kt | 4 +- .../system/display/DisplayAdapter.kt | 2 +- .../system/files/AndroidFileAdapter.kt | 8 +- .../system/files/DocumentFileWrapper.kt | 82 ++++++++++--------- .../system/foldable/AndroidFoldableAdapter.kt | 4 +- .../system/inputevents/KMEvdevEvent.kt | 2 +- .../system/inputevents/KMInputEvent.kt | 2 +- .../system/inputevents/KeyEventUtils.kt | 5 +- .../keymapper/system/inputevents/Scancode.kt | 2 +- .../KeyEventRelayServiceWrapper.kt | 11 +-- .../system/inputmethod/KeyMapperImeService.kt | 8 +- .../keymapper/system/intents/IntentAdapter.kt | 3 +- .../system/intents/IntentExtraModel.kt | 2 +- .../system/leanback/LeanbackAdapter.kt | 5 +- .../system/lock/AndroidLockScreenAdapter.kt | 8 +- .../system/lock/LockScreenAdapter.kt | 1 - .../system/media/AndroidMediaAdapter.kt | 42 ++++++---- .../keymapper/system/media/MediaAdapter.kt | 1 - .../system/network/AndroidNetworkAdapter.kt | 8 +- .../system/network/NetworkAdapter.kt | 1 - .../keymapper/system/nfc/AndroidNfcAdapter.kt | 4 +- .../sds100/keymapper/system/nfc/NfcAdapter.kt | 1 - .../notifications/NotificationChannelModel.kt | 7 +- .../system/notifications/NotificationModel.kt | 4 +- .../notifications/NotificationReceiver.kt | 11 ++- .../NotificationReceiverAdapterImpl.kt | 6 +- .../notifications/NotificationRemoteInput.kt | 4 +- .../notifications/NotificationServiceEvent.kt | 3 +- .../ObserveNotificationListenersJob.kt | 6 +- .../permissions/AndroidPermissionAdapter.kt | 18 ++-- .../system/permissions/PermissionAdapter.kt | 1 - .../permissions/SystemFeatureAdapter.kt | 1 - .../system/phone/AndroidPhoneAdapter.kt | 14 ++-- .../keymapper/system/phone/PhoneAdapter.kt | 1 - .../system/popup/AndroidToastAdapter.kt | 5 +- .../keymapper/system/popup/ToastAdapter.kt | 1 - .../system/power/AndroidPowerAdapter.kt | 11 ++- .../keymapper/system/power/PowerAdapter.kt | 1 - .../system/ringtones/RingtoneAdapter.kt | 3 +- .../system/shell/BaseShellAdapter.kt | 78 +++++++++--------- .../keymapper/system/shell/ShellAdapter.kt | 7 +- .../system/shell/StandardShellAdapter.kt | 2 +- .../system/shizuku/ShizukuAdapter.kt | 1 - .../system/shizuku/ShizukuAdapterImpl.kt | 4 +- .../keymapper/system/shizuku/ShizukuUtils.kt | 1 - .../system/url/AndroidOpenUrlAdapter.kt | 7 +- .../keymapper/system/url/OpenUrlAdapter.kt | 1 - .../sds100/keymapper/system/url/UrlUtils.kt | 2 - .../system/vibrator/AndroidVibratorAdapter.kt | 5 +- .../system/vibrator/VibratorAdapter.kt | 1 - .../system/volume/AndroidVolumeAdapter.kt | 16 ++-- .../keymapper/system/volume/RingerMode.kt | 1 - .../keymapper/system/volume/VolumeAdapter.kt | 1 - .../keymapper/system/volume/VolumeStream.kt | 1 - 71 files changed, 262 insertions(+), 271 deletions(-) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/AndroidSystemFeatureAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/AndroidSystemFeatureAdapter.kt index 0e92208654..6f0c9ed824 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/AndroidSystemFeatureAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/AndroidSystemFeatureAdapter.kt @@ -2,17 +2,18 @@ package io.github.sds100.keymapper.system import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.system.permissions.SystemFeatureAdapter import javax.inject.Inject import javax.inject.Singleton -import io.github.sds100.keymapper.system.permissions.SystemFeatureAdapter @Singleton class AndroidSystemFeatureAdapter @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, ) : SystemFeatureAdapter { private val ctx = context.applicationContext - override fun hasSystemFeature(feature: String): Boolean = ctx.packageManager.hasSystemFeature(feature) + override fun hasSystemFeature(feature: String): Boolean = + ctx.packageManager.hasSystemFeature(feature) override fun getSystemFeatures(): List { return ctx.packageManager.systemAvailableFeatures.map { it.name } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/JobSchedulerHelper.kt b/system/src/main/java/io/github/sds100/keymapper/system/JobSchedulerHelper.kt index c0eb5e1777..7bf619688a 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/JobSchedulerHelper.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/JobSchedulerHelper.kt @@ -74,7 +74,9 @@ object JobSchedulerHelper { ) val historyContentUri = JobInfo.TriggerContentUri( - Settings.Secure.getUriFor(AndroidInputMethodAdapter.SETTINGS_SECURE_SUBTYPE_HISTORY_KEY), + Settings.Secure.getUriFor( + AndroidInputMethodAdapter.SETTINGS_SECURE_SUBTYPE_HISTORY_KEY, + ), JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS, ) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt b/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt index 65460856f8..a151730aa5 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt @@ -82,7 +82,9 @@ abstract class SystemHiltModule { @Singleton @Binds - abstract fun providePackageManagerAdapter(impl: AndroidPackageManagerAdapter): PackageManagerAdapter + abstract fun providePackageManagerAdapter( + impl: AndroidPackageManagerAdapter, + ): PackageManagerAdapter @Singleton @Binds @@ -182,9 +184,13 @@ abstract class SystemHiltModule { @Singleton @Binds - abstract fun provideNotificationReceiverAdapter(impl: NotificationReceiverAdapterImpl): NotificationReceiverAdapter + abstract fun provideNotificationReceiverAdapter( + impl: NotificationReceiverAdapterImpl, + ): NotificationReceiverAdapter @Singleton @Binds - abstract fun provideSystemFeatureAdapter(impl: AndroidSystemFeatureAdapter): SystemFeatureAdapter + abstract fun provideSystemFeatureAdapter( + impl: AndroidSystemFeatureAdapter, + ): SystemFeatureAdapter } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceState.kt b/system/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceState.kt index f762a294b5..dfaf87c5f9 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceState.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceState.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.system.accessibility - enum class AccessibilityServiceState { ENABLED, CRASHED, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt index d4360093a2..00cbe0cff3 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt @@ -11,17 +11,17 @@ import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch @Singleton class AndroidAirplaneModeAdapter @Inject constructor( @ApplicationContext private val ctx: Context, private val systemBridgeConnectionManager: SystemBridgeConnectionManager, private val suAdapter: SuAdapter, - private val coroutineScope: CoroutineScope + private val coroutineScope: CoroutineScope, ) : AirplaneModeAdapter { override suspend fun enable(): KMResult<*> { @@ -57,7 +57,9 @@ class AndroidAirplaneModeAdapter @Inject constructor( private fun broadcastAirplaneModeChanged(enabled: Boolean) { coroutineScope.launch { - suAdapter.execute("am broadcast -a android.intent.action.AIRPLANE_MODE --ez state $enabled") + suAdapter.execute( + "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state $enabled", + ) } } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/apps/ActivityInfo.kt b/system/src/main/java/io/github/sds100/keymapper/system/apps/ActivityInfo.kt index 3504e71246..733f072aa6 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/apps/ActivityInfo.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/apps/ActivityInfo.kt @@ -3,7 +3,4 @@ package io.github.sds100.keymapper.system.apps import kotlinx.serialization.Serializable @Serializable -data class ActivityInfo( - val activityName: String, - val packageName: String, -) +data class ActivityInfo(val activityName: String, val packageName: String) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt index 69072392a7..bb629806ca 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt @@ -19,11 +19,11 @@ import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.success -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow @Singleton class AndroidAppShortcutAdapter @Inject constructor( @@ -106,7 +106,8 @@ class AndroidAppShortcutAdapter @Inject constructor( } } - override fun createShortcutResultIntent(shortcut: ShortcutInfoCompat): Intent = ShortcutManagerCompat.createShortcutResultIntent(ctx, shortcut) + override fun createShortcutResultIntent(shortcut: ShortcutInfoCompat): Intent = + ShortcutManagerCompat.createShortcutResultIntent(ctx, shortcut) override fun getShortcutName(info: AppShortcutInfo): KMResult { try { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt index e1d2c0546b..40fea585c5 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt @@ -29,6 +29,9 @@ import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.success +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -38,9 +41,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.IOException -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AndroidPackageManagerAdapter @Inject constructor( @@ -234,7 +234,8 @@ class AndroidPackageManagerAdapter @Inject constructor( } } - @SuppressLint("UnspecifiedImmutableFlag") // only specify the flag on SDK 23+. SDK 31 is first to enforce it. + // only specify the flag on SDK 23+. SDK 31 is first to enforce it. + @SuppressLint("UnspecifiedImmutableFlag") override fun openApp(packageName: String): KMResult<*> { val leanbackIntent = packageManager.getLeanbackLaunchIntentForPackage(packageName) val normalIntent = packageManager.getLaunchIntentForPackage(packageName) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/apps/AppShortcutInfo.kt b/system/src/main/java/io/github/sds100/keymapper/system/apps/AppShortcutInfo.kt index 2b2bbc45e7..6c7c824303 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/apps/AppShortcutInfo.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/apps/AppShortcutInfo.kt @@ -1,6 +1,3 @@ package io.github.sds100.keymapper.system.apps -data class AppShortcutInfo( - val packageName: String, - val activityName: String, -) +data class AppShortcutInfo(val packageName: String, val activityName: String) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/apps/KeyMapShortcutActivityIntentBuilder.kt b/system/src/main/java/io/github/sds100/keymapper/system/apps/KeyMapShortcutActivityIntentBuilder.kt index b25a2cac13..47a766b5bc 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/apps/KeyMapShortcutActivityIntentBuilder.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/apps/KeyMapShortcutActivityIntentBuilder.kt @@ -4,8 +4,5 @@ import android.content.Intent import android.os.Bundle interface KeyMapShortcutActivityIntentBuilder { - fun build( - intentAction: String, - intentExtras: Bundle, - ): Intent + fun build(intentAction: String, intentExtras: Bundle): Intent } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/apps/PackageInfo.kt b/system/src/main/java/io/github/sds100/keymapper/system/apps/PackageInfo.kt index d5fedd7bdb..f6c4dfad47 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/apps/PackageInfo.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/apps/PackageInfo.kt @@ -1,7 +1,5 @@ package io.github.sds100.keymapper.system.apps - - data class PackageInfo( val packageName: String, val activities: List, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/apps/PackageManagerAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/apps/PackageManagerAdapter.kt index d06cdcf040..191f1fb174 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/apps/PackageManagerAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/apps/PackageManagerAdapter.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow - interface PackageManagerAdapter { val onPackagesChanged: Flow val installedPackages: StateFlow>> diff --git a/system/src/main/java/io/github/sds100/keymapper/system/apps/TrampolineActivity.kt b/system/src/main/java/io/github/sds100/keymapper/system/apps/TrampolineActivity.kt index 89fc8b24fa..eed1857cd9 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/apps/TrampolineActivity.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/apps/TrampolineActivity.kt @@ -51,4 +51,4 @@ class TrampolineActivity : ComponentActivity() { finish() } -} \ No newline at end of file +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt index 838a455be7..547c6a5d1a 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt @@ -16,19 +16,19 @@ import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AndroidBluetoothAdapter @Inject constructor( @ApplicationContext private val context: Context, private val coroutineScope: CoroutineScope, - private val systemBridgeConnectionManager: SystemBridgeConnectionManager + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, ) : io.github.sds100.keymapper.system.bluetooth.BluetoothAdapter { private val bluetoothManager: BluetoothManager? = context.getSystemService() @@ -153,7 +153,6 @@ class AndroidBluetoothAdapter @Inject constructor( adapter.enable() return Success(Unit) } - } override fun disable(): KMResult<*> { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/camera/AndroidCameraAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/camera/AndroidCameraAdapter.kt index ff90c12619..7c9def425d 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/camera/AndroidCameraAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/camera/AndroidCameraAdapter.kt @@ -10,18 +10,17 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton @Singleton -class AndroidCameraAdapter @Inject constructor( - @ApplicationContext private val context: Context -) : CameraAdapter { +class AndroidCameraAdapter @Inject constructor(@ApplicationContext private val context: Context) : + CameraAdapter { private val ctx = context.applicationContext private val cameraManager: CameraManager by lazy { ctx.getSystemService()!! } @@ -197,7 +196,6 @@ class AndroidCameraAdapter @Inject constructor( lens: CameraLens, strengthPercent: Float? = null, ): KMResult<*> { - try { val cameraId = getFlashlightCameraIdForLens(lens) val flashInfo = getFlashInfo(lens) @@ -210,7 +208,10 @@ class AndroidCameraAdapter @Inject constructor( } // try to find a camera with a flash - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && enabled && flashInfo.supportsVariableStrength) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + enabled && + flashInfo.supportsVariableStrength + ) { val strength = if (strengthPercent == null) { flashInfo.defaultStrength } else { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/clipboard/AndroidClipboardAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/clipboard/AndroidClipboardAdapter.kt index ed4051ef21..a46d0d994e 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/clipboard/AndroidClipboardAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/clipboard/AndroidClipboardAdapter.kt @@ -10,7 +10,7 @@ import javax.inject.Singleton @Singleton class AndroidClipboardAdapter @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, ) : ClipboardAdapter { private val ctx = context.applicationContext diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index 72bd687201..923dc8972d 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -19,6 +19,8 @@ import io.github.sds100.keymapper.common.utils.ifIsData import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -28,8 +30,6 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AndroidDevicesAdapter @Inject constructor( @@ -75,7 +75,9 @@ class AndroidDevicesAdapter @Inject constructor( override fun onInputDeviceAdded(deviceId: Int) { coroutineScope.launch { val device = InputDevice.getDevice(deviceId) ?: return@launch - onInputDeviceConnect.emit(InputDeviceUtils.createInputDeviceInfo(device)) + onInputDeviceConnect.emit( + InputDeviceUtils.createInputDeviceInfo(device), + ) updateInputDevices() } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt index 9067b25632..d234b680aa 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt @@ -18,6 +18,8 @@ import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.getRealDisplaySize +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -25,8 +27,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AndroidDisplayAdapter @Inject constructor( diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt index a38fe5a177..f8792d6fd4 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt @@ -1,8 +1,8 @@ package io.github.sds100.keymapper.system.display import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.common.utils.Orientation +import io.github.sds100.keymapper.common.utils.SizeKM import kotlinx.coroutines.flow.Flow interface DisplayAdapter { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/files/AndroidFileAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/files/AndroidFileAdapter.kt index 18a9a553e6..04a151647b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/files/AndroidFileAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/files/AndroidFileAdapter.kt @@ -15,19 +15,19 @@ import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import net.lingala.zip4j.ZipFile import java.io.File import java.io.InputStream import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.lingala.zip4j.ZipFile @Singleton class AndroidFileAdapter @Inject constructor( @ApplicationContext private val context: Context, - private val buildConfigProvider: BuildConfigProvider + private val buildConfigProvider: BuildConfigProvider, ) : FileAdapter { private val ctx = context.applicationContext private val contentResolver: ContentResolver = ctx.contentResolver diff --git a/system/src/main/java/io/github/sds100/keymapper/system/files/DocumentFileWrapper.kt b/system/src/main/java/io/github/sds100/keymapper/system/files/DocumentFileWrapper.kt index 1bf21fb57e..45a26f5be2 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/files/DocumentFileWrapper.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/files/DocumentFileWrapper.kt @@ -15,13 +15,13 @@ import com.anggrayudi.storage.media.FileDescription import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import java.io.File import java.io.InputStream import java.io.OutputStream import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class DocumentFileWrapper(val file: DocumentFile, context: Context) : IFile { @@ -90,51 +90,53 @@ class DocumentFileWrapper(val file: DocumentFile, context: Context) : IFile { } } - override suspend fun copyTo(directory: IFile, fileName: String?): KMResult<*> = withContext(Dispatchers.Default) { - suspendCoroutine { continuation -> - val callback = object : FileCallback() { - override fun onCompleted(result: Any) { - super.onCompleted(result) + override suspend fun copyTo(directory: IFile, fileName: String?): KMResult<*> = + withContext(Dispatchers.Default) { + suspendCoroutine { continuation -> + val callback = object : FileCallback() { + override fun onCompleted(result: Any) { + super.onCompleted(result) - continuation.resume(Success(Unit)) - } + continuation.resume(Success(Unit)) + } - override fun onFailed(errorCode: ErrorCode) { - super.onFailed(errorCode) - - val error = when (errorCode) { - ErrorCode.STORAGE_PERMISSION_DENIED -> KMError.StoragePermissionDenied - ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> KMError.CannotCreateFileInTarget( - directory.uri, - ) - - ErrorCode.SOURCE_FILE_NOT_FOUND -> KMError.SourceFileNotFound(this@DocumentFileWrapper.uri) - ErrorCode.TARGET_FILE_NOT_FOUND -> KMError.TargetFileNotFound(directory.uri) - ErrorCode.TARGET_FOLDER_NOT_FOUND -> KMError.TargetDirectoryNotFound( - directory.uri, - ) - - ErrorCode.UNKNOWN_IO_ERROR -> KMError.UnknownIOError - ErrorCode.CANCELED -> KMError.FileOperationCancelled - ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> KMError.TargetDirectoryMatchesSourceDirectory - ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> KMError.NoSpaceLeftOnTarget( - directory.uri, - ) + override fun onFailed(errorCode: ErrorCode) { + super.onFailed(errorCode) + + val error = when (errorCode) { + ErrorCode.STORAGE_PERMISSION_DENIED -> KMError.StoragePermissionDenied + ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> + KMError.CannotCreateFileInTarget(directory.uri) + + ErrorCode.SOURCE_FILE_NOT_FOUND -> + KMError.SourceFileNotFound(this@DocumentFileWrapper.uri) + ErrorCode.TARGET_FILE_NOT_FOUND -> + KMError.TargetFileNotFound(directory.uri) + ErrorCode.TARGET_FOLDER_NOT_FOUND -> + KMError.TargetDirectoryNotFound(directory.uri) + + ErrorCode.UNKNOWN_IO_ERROR -> KMError.UnknownIOError + ErrorCode.CANCELED -> KMError.FileOperationCancelled + ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> + KMError.TargetDirectoryMatchesSourceDirectory + ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> KMError.NoSpaceLeftOnTarget( + directory.uri, + ) + } + + continuation.resume(error) } - continuation.resume(error) + override fun onStart(file: Any, workerThread: Thread): Long = 0 } - override fun onStart(file: Any, workerThread: Thread): Long = 0 - } + val fileDescription = if (fileName == null) { + null + } else { + FileDescription(fileName) + } - val fileDescription = if (fileName == null) { - null - } else { - FileDescription(fileName) + file.copyFileTo(ctx, directory.path, fileDescription, callback) } - - file.copyFileTo(ctx, directory.path, fileDescription, callback) } - } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/foldable/AndroidFoldableAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/foldable/AndroidFoldableAdapter.kt index 39c87139ed..9f96f21ed1 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/foldable/AndroidFoldableAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/foldable/AndroidFoldableAdapter.kt @@ -9,12 +9,12 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton @RequiresApi(Build.VERSION_CODES.R) @Singleton diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt index 424f869477..fa88349920 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt @@ -9,7 +9,7 @@ data class KMEvdevEvent( val value: Int, val androidCode: Int, val timeSec: Long, - val timeUsec: Long + val timeUsec: Long, ) : KMInputEvent { companion object { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt index 1e12eaed08..73ac5637f9 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt @@ -1,3 +1,3 @@ package io.github.sds100.keymapper.system.inputevents -sealed interface KMInputEvent \ No newline at end of file +sealed interface KMInputEvent diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt index 1331dcd998..9c3a04080c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt @@ -390,7 +390,9 @@ object KeyEventUtils { KeyEvent.KEYCODE_ALT_RIGHT -> KeyEvent.META_ALT_RIGHT_ON.withFlag(KeyEvent.META_ALT_ON) KeyEvent.KEYCODE_SHIFT_LEFT -> KeyEvent.META_SHIFT_LEFT_ON.withFlag(KeyEvent.META_SHIFT_ON) - KeyEvent.KEYCODE_SHIFT_RIGHT -> KeyEvent.META_SHIFT_RIGHT_ON.withFlag(KeyEvent.META_SHIFT_ON) + KeyEvent.KEYCODE_SHIFT_RIGHT -> KeyEvent.META_SHIFT_RIGHT_ON.withFlag( + KeyEvent.META_SHIFT_ON, + ) KeyEvent.KEYCODE_SYM -> KeyEvent.META_SYM_ON @@ -472,4 +474,3 @@ object KeyEventUtils { scanCode == Scancode.KEY_POWER2 } } - diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt index bc2d90fcd3..3e009a07ab 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt @@ -632,4 +632,4 @@ object Scancode { const val BTN_TRIGGER_HAPPY38 = 0x2e5 const val BTN_TRIGGER_HAPPY39 = 0x2e6 const val BTN_TRIGGER_HAPPY40 = 0x2e7 -} \ No newline at end of file +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt index a6d1e94c93..f694d32661 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt @@ -19,7 +19,7 @@ import javax.inject.Singleton @Singleton class KeyEventRelayServiceWrapperImpl @Inject constructor( @ApplicationContext private val ctx: Context, - private val buildConfigProvider: BuildConfigProvider + private val buildConfigProvider: BuildConfigProvider, ) : KeyEventRelayServiceWrapper { private val keyEventRelayServiceLock: Any = Any() @@ -27,10 +27,7 @@ class KeyEventRelayServiceWrapperImpl @Inject constructor( private val serviceConnection: ServiceConnection = object : ServiceConnection { - override fun onServiceConnected( - name: ComponentName?, - service: IBinder?, - ) { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { synchronized(keyEventRelayServiceLock) { keyEventRelayService = IKeyEventRelayService.Stub.asInterface(service) } @@ -95,7 +92,7 @@ class KeyEventRelayServiceWrapperImpl @Inject constructor( val component = ComponentName( buildConfigProvider.packageName, - "io.github.sds100.keymapper.api.KeyEventRelayService" + "io.github.sds100.keymapper.api.KeyEventRelayService", ) relayServiceIntent.setComponent(component) val isSuccess = @@ -127,4 +124,4 @@ interface KeyEventRelayServiceWrapper { fun unregisterClient(id: String) fun sendKeyEvent(event: KeyEvent, targetPackageName: String, callbackId: String): Boolean fun sendMotionEvent(event: MotionEvent, targetPackageName: String, callbackId: String): Boolean -} \ No newline at end of file +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt index 94ee5d311c..fa3b64167c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt @@ -20,8 +20,8 @@ import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent -import timber.log.Timber import javax.inject.Inject +import timber.log.Timber /** * DO NOT MOVE. Must stay in this package so the user's input method settings are not reset @@ -142,7 +142,7 @@ class KeyMapperImeService : InputMethodService() { keyEventRelayServiceWrapper.registerClient( CALLBACK_ID_INPUT_METHOD, - keyEventReceiverCallback + keyEventReceiverCallback, ) } @@ -164,8 +164,8 @@ class KeyMapperImeService : InputMethodService() { serviceAdapter.sendAsync( AccessibilityServiceEvent.OnKeyMapperImeStartInput( attribute = attribute, - restarting = restarting - ) + restarting = restarting, + ), ) } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/intents/IntentAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/intents/IntentAdapter.kt index ebb6d120d1..972cb202b5 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/intents/IntentAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/intents/IntentAdapter.kt @@ -10,7 +10,8 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class IntentAdapterImpl @Inject constructor(@ApplicationContext private val context: Context) : IntentAdapter { +class IntentAdapterImpl @Inject constructor(@ApplicationContext private val context: Context) : + IntentAdapter { private val ctx = context.applicationContext override fun send( diff --git a/system/src/main/java/io/github/sds100/keymapper/system/intents/IntentExtraModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/intents/IntentExtraModel.kt index 46f4ec46d8..d136aee215 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/intents/IntentExtraModel.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/intents/IntentExtraModel.kt @@ -1,7 +1,7 @@ package io.github.sds100.keymapper.system.intents -import kotlinx.serialization.Serializable import java.util.UUID +import kotlinx.serialization.Serializable @Serializable data class IntentExtraModel( diff --git a/system/src/main/java/io/github/sds100/keymapper/system/leanback/LeanbackAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/leanback/LeanbackAdapter.kt index 059208e275..6ebc826a6e 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/leanback/LeanbackAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/leanback/LeanbackAdapter.kt @@ -9,9 +9,8 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class LeanbackAdapterImpl @Inject constructor( - @ApplicationContext context: Context, -) : LeanbackAdapter { +class LeanbackAdapterImpl @Inject constructor(@ApplicationContext context: Context) : + LeanbackAdapter { private val ctx = context.applicationContext override fun isTvDevice(): Boolean { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/lock/AndroidLockScreenAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/lock/AndroidLockScreenAdapter.kt index 07b30cb2e5..6b11487316 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/lock/AndroidLockScreenAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/lock/AndroidLockScreenAdapter.kt @@ -11,10 +11,10 @@ import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import javax.inject.Inject class AndroidLockScreenAdapter @Inject constructor(@ApplicationContext private val ctx: Context) : LockScreenAdapter { @@ -28,7 +28,11 @@ class AndroidLockScreenAdapter @Inject constructor(@ApplicationContext private v context ?: return when (intent.action) { - Intent.ACTION_SCREEN_ON, Intent.ACTION_SCREEN_OFF, Intent.ACTION_USER_PRESENT, Intent.ACTION_USER_UNLOCKED -> { + Intent.ACTION_SCREEN_ON, + Intent.ACTION_SCREEN_OFF, + Intent.ACTION_USER_PRESENT, + Intent.ACTION_USER_UNLOCKED, + -> { isLockedFlow.update { isLocked() } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/lock/LockScreenAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/lock/LockScreenAdapter.kt index e5d5c44095..1b91b62c98 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/lock/LockScreenAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/lock/LockScreenAdapter.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.system.lock import io.github.sds100.keymapper.common.utils.KMResult import kotlinx.coroutines.flow.Flow - interface LockScreenAdapter { fun secureLockDevice(): KMResult<*> diff --git a/system/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt index d764a2945e..d7461da2a3 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt @@ -12,25 +12,25 @@ import android.os.Build import android.view.KeyEvent import androidx.annotation.RequiresApi import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.system.volume.VolumeStream +import java.io.FileNotFoundException +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.io.FileNotFoundException -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AndroidMediaAdapter @Inject constructor( @ApplicationContext private val context: Context, - private val coroutineScope: CoroutineScope + private val coroutineScope: CoroutineScope, ) : MediaAdapter { private val ctx = context.applicationContext @@ -49,7 +49,9 @@ class AndroidMediaAdapter @Inject constructor( private val audioPlaybackCallback by lazy { @RequiresApi(Build.VERSION_CODES.O) object : AudioManager.AudioPlaybackCallback() { - override fun onPlaybackConfigChanged(configs: MutableList?) { + override fun onPlaybackConfigChanged( + configs: MutableList?, + ) { audioVolumeControlStreams.update { getActiveAudioVolumeStreams() } } } @@ -72,21 +74,29 @@ class AndroidMediaAdapter @Inject constructor( } } - override fun fastForward(packageName: String?): KMResult<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, packageName) + override fun fastForward(packageName: String?): KMResult<*> = + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, packageName) - override fun rewind(packageName: String?): KMResult<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND, packageName) + override fun rewind(packageName: String?): KMResult<*> = + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND, packageName) - override fun play(packageName: String?): KMResult<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY, packageName) + override fun play(packageName: String?): KMResult<*> = + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY, packageName) - override fun pause(packageName: String?): KMResult<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE, packageName) + override fun pause(packageName: String?): KMResult<*> = + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE, packageName) - override fun playPause(packageName: String?): KMResult<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, packageName) + override fun playPause(packageName: String?): KMResult<*> = + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, packageName) - override fun previousTrack(packageName: String?): KMResult<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS, packageName) + override fun previousTrack(packageName: String?): KMResult<*> = + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS, packageName) - override fun nextTrack(packageName: String?): KMResult<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT, packageName) + override fun nextTrack(packageName: String?): KMResult<*> = + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT, packageName) - override fun stop(packageName: String?): KMResult<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP, packageName) + override fun stop(packageName: String?): KMResult<*> = + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP, packageName) override fun stopFileMedia(): KMResult<*> { synchronized(mediaPlayerLock) { @@ -148,7 +158,9 @@ class AndroidMediaAdapter @Inject constructor( mediaPlayer = MediaPlayer().apply { val usage = when (stream) { VolumeStream.ACCESSIBILITY -> AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY - else -> throw Exception("Don't know how to convert volume stream to audio usage attribute") + else -> throw Exception( + "Don't know how to convert volume stream to audio usage attribute", + ) } setAudioAttributes( diff --git a/system/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt index 79e16d53dc..833058281e 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt @@ -6,7 +6,6 @@ import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.system.volume.VolumeStream import kotlinx.coroutines.flow.Flow - interface MediaAdapter { /** diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index 27dc8de180..1ecd1b5116 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -26,6 +26,8 @@ import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -39,14 +41,12 @@ import okhttp3.RequestBody.Companion.toRequestBody import okio.IOException import okio.use import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AndroidNetworkAdapter @Inject constructor( @ApplicationContext private val context: Context, private val suAdapter: SuAdapter, - private val systemBridgeConnManager: SystemBridgeConnectionManager + private val systemBridgeConnManager: SystemBridgeConnectionManager, ) : NetworkAdapter { private val ctx = context.applicationContext private val wifiManager: WifiManager by lazy { ctx.getSystemService()!! } @@ -101,7 +101,7 @@ class AndroidNetworkAdapter @Inject constructor( override fun onCapabilitiesChanged( network: Network, - networkCapabilities: NetworkCapabilities + networkCapabilities: NetworkCapabilities, ) { super.onCapabilitiesChanged(network, networkCapabilities) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt index 27862c975e..1440caa651 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.system.network import io.github.sds100.keymapper.common.utils.KMResult import kotlinx.coroutines.flow.Flow - interface NetworkAdapter { val connectedWifiSSIDFlow: Flow val isWifiConnected: Flow diff --git a/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt index d96b12a4e6..a57a5c3462 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt @@ -9,15 +9,15 @@ import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter -import kotlinx.coroutines.runBlocking import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.runBlocking @Singleton class AndroidNfcAdapter @Inject constructor( @ApplicationContext private val context: Context, private val suAdapter: SuAdapter, - private val systemBridgeConnectionManager: SystemBridgeConnectionManager + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, ) : NfcAdapter { private val ctx = context.applicationContext diff --git a/system/src/main/java/io/github/sds100/keymapper/system/nfc/NfcAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/nfc/NfcAdapter.kt index e155ffd1e4..284caa2bcc 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/nfc/NfcAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/nfc/NfcAdapter.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.system.nfc import io.github.sds100.keymapper.common.utils.KMResult - interface NfcAdapter { fun isEnabled(): Boolean fun enable(): KMResult<*> diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationChannelModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationChannelModel.kt index f716f1882b..50aa8d0771 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationChannelModel.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationChannelModel.kt @@ -1,8 +1,3 @@ package io.github.sds100.keymapper.system.notifications - -data class NotificationChannelModel( - val id: String, - val name: String, - val importance: Int, -) +data class NotificationChannelModel(val id: String, val name: String, val importance: Int) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt index 2b0e1c1f9a..efc1cd8926 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt @@ -4,7 +4,6 @@ import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat import io.github.sds100.keymapper.common.notifications.KMNotificationAction - data class NotificationModel( val id: Int, val channel: String, @@ -34,6 +33,5 @@ data class NotificationModel( val bigTextStyle: Boolean = false, val silent: Boolean = false, val showIndeterminateProgress: Boolean = false, - val timeout: Long? = null + val timeout: Long? = null, ) - diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationReceiver.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationReceiver.kt index 6d8e80bc5c..974b2948c5 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationReceiver.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationReceiver.kt @@ -11,13 +11,15 @@ import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.system.media.AndroidMediaAdapter +import javax.inject.Inject import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber -import javax.inject.Inject @AndroidEntryPoint -class NotificationReceiver : NotificationListenerService(), LifecycleOwner { +class NotificationReceiver : + NotificationListenerService(), + LifecycleOwner { private val mediaSessionManager: MediaSessionManager by lazy { getSystemService()!! } private val notificationListenerComponent by lazy { @@ -37,7 +39,6 @@ class NotificationReceiver : NotificationListenerService(), LifecycleOwner { private var lastNotificationKey: String? = null - @Inject lateinit var serviceAdapter: NotificationReceiverAdapterImpl @@ -52,7 +53,9 @@ class NotificationReceiver : NotificationListenerService(), LifecycleOwner { serviceAdapter.eventsToService .onEach { event -> when (event) { - NotificationServiceEvent.DismissLastNotification -> cancelNotification(lastNotificationKey) + NotificationServiceEvent.DismissLastNotification -> cancelNotification( + lastNotificationKey, + ) NotificationServiceEvent.DismissAllNotifications -> cancelAllNotifications() else -> Unit } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationReceiverAdapterImpl.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationReceiverAdapterImpl.kt index 401479513e..e1b8578207 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationReceiverAdapterImpl.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationReceiverAdapterImpl.kt @@ -18,13 +18,13 @@ import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.system.JobSchedulerHelper import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton @Singleton class NotificationReceiverAdapterImpl @Inject constructor( @@ -41,7 +41,7 @@ class NotificationReceiverAdapterImpl @Inject constructor( init { // use job scheduler because there is there is a much shorter delay when the app is in the background if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - JobSchedulerHelper.observeEnabledNotificationListeners(ctx) + JobSchedulerHelper.observeEnabledNotificationListeners(ctx) } else { val uri = Settings.Secure.getUriFor("enabled_notification_listeners") val observer = object : ContentObserver(Handler(Looper.getMainLooper())) { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt index 9e536e8de4..f7ceff6139 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt @@ -9,5 +9,5 @@ import io.github.sds100.keymapper.common.notifications.KMNotificationAction */ data class NotificationRemoteInput( val intentAction: KMNotificationAction.IntentAction, - val text: String -) \ No newline at end of file + val text: String, +) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationServiceEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationServiceEvent.kt index 42843285eb..cfc6fe15bb 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationServiceEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationServiceEvent.kt @@ -9,5 +9,4 @@ sealed class NotificationServiceEvent { @Serializable data object DismissAllNotifications : NotificationServiceEvent() - -} \ No newline at end of file +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/ObserveNotificationListenersJob.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/ObserveNotificationListenersJob.kt index d01039e34d..bab2ebb599 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/ObserveNotificationListenersJob.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/ObserveNotificationListenersJob.kt @@ -10,12 +10,12 @@ import javax.inject.Inject @AndroidEntryPoint class ObserveNotificationListenersJob : JobService() { - + @Inject lateinit var permissionAdapter: AndroidPermissionAdapter - + override fun onStartJob(params: JobParameters?): Boolean { - permissionAdapter.onPermissionsChanged() + permissionAdapter.onPermissionsChanged() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { JobSchedulerHelper.observeEnabledNotificationListeners(applicationContext) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt index f280d50a2f..6054aa3c9c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt @@ -20,7 +20,6 @@ import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.getIdentifier import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess @@ -29,12 +28,14 @@ import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager -import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.manager.isConnected import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.DeviceAdmin import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -54,8 +55,6 @@ import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.SystemServiceHelper import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AndroidPermissionAdapter @Inject constructor( @@ -66,7 +65,7 @@ class AndroidPermissionAdapter @Inject constructor( private val preferenceRepository: PreferenceRepository, private val buildConfigProvider: BuildConfigProvider, private val systemBridgeConnectionManager: SystemBridgeConnectionManager, - private val shizukuAdapter: ShizukuAdapter + private val shizukuAdapter: ShizukuAdapter, ) : PermissionAdapter { companion object { const val REQUEST_CODE_SHIZUKU_PERMISSION = 1 @@ -153,7 +152,7 @@ class AndroidPermissionAdapter @Inject constructor( val isSystemBridgeConnected = Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API && - systemBridgeConnectionManager.connectionState.firstBlocking() is SystemBridgeConnectionState.Connected + systemBridgeConnectionManager.isConnected() if (isSystemBridgeConnected) { result = systemBridgeConnectionManager.run { bridge -> @@ -169,8 +168,11 @@ class AndroidPermissionAdapter @Inject constructor( val userId = Process.myUserHandle()!!.getIdentifier() PermissionManagerApis.grantPermission( - shizukuPermissionManager, buildConfigProvider.packageName, - permissionName, deviceId, userId + shizukuPermissionManager, + buildConfigProvider.packageName, + permissionName, + deviceId, + userId, ) if (ContextCompat.checkSelfPermission(ctx, permissionName) == PERMISSION_GRANTED) { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/PermissionAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/PermissionAdapter.kt index 62427cf3ce..cefb5c2a30 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/PermissionAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/PermissionAdapter.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.system.permissions import io.github.sds100.keymapper.common.utils.KMResult import kotlinx.coroutines.flow.Flow - interface PermissionAdapter { val onPermissionsUpdate: Flow fun isGranted(permission: Permission): Boolean diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/SystemFeatureAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/SystemFeatureAdapter.kt index dd7474a841..287c36e7a0 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/SystemFeatureAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/SystemFeatureAdapter.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.system.permissions - interface SystemFeatureAdapter { fun hasSystemFeature(feature: String): Boolean fun getSystemFeatures(): List diff --git a/system/src/main/java/io/github/sds100/keymapper/system/phone/AndroidPhoneAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/phone/AndroidPhoneAdapter.kt index 934c8635ff..eadc547009 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/phone/AndroidPhoneAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/phone/AndroidPhoneAdapter.kt @@ -24,6 +24,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel @@ -31,8 +33,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AndroidPhoneAdapter @Inject constructor( @@ -111,7 +111,7 @@ class AndroidPhoneAdapter @Inject constructor( ctx, broadcastReceiver, this, - ContextCompat.RECEIVER_EXPORTED + ContextCompat.RECEIVER_EXPORTED, ) } } @@ -150,7 +150,7 @@ class AndroidPhoneAdapter @Inject constructor( private fun hasAnswerPhoneCallsPermission(): Boolean { return ActivityCompat.checkSelfPermission( ctx, - Manifest.permission.ANSWER_PHONE_CALLS + Manifest.permission.ANSWER_PHONE_CALLS, ) == PackageManager.PERMISSION_GRANTED } @@ -181,7 +181,7 @@ class AndroidPhoneAdapter @Inject constructor( ctx, 0, Intent(ACTION_SMS_SENT_RESULT), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) try { @@ -234,7 +234,9 @@ class AndroidPhoneAdapter @Inject constructor( TelephonyManager.CALL_STATE_IDLE -> return CallState.NONE TelephonyManager.CALL_STATE_OFFHOOK -> return CallState.IN_PHONE_CALL TelephonyManager.CALL_STATE_RINGING -> return CallState.RINGING - else -> throw IllegalArgumentException("Don't know how to convert that call state $sdkCallState") + else -> throw IllegalArgumentException( + "Don't know how to convert that call state $sdkCallState", + ) } } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/phone/PhoneAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/phone/PhoneAdapter.kt index d1b6e47239..5229852025 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/phone/PhoneAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/phone/PhoneAdapter.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.system.phone import io.github.sds100.keymapper.common.utils.KMResult import kotlinx.coroutines.flow.Flow - interface PhoneAdapter { val callStateFlow: Flow diff --git a/system/src/main/java/io/github/sds100/keymapper/system/popup/AndroidToastAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/popup/AndroidToastAdapter.kt index b4e7e7cf0a..a9086a8810 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/popup/AndroidToastAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/popup/AndroidToastAdapter.kt @@ -7,9 +7,8 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class AndroidToastAdapter @Inject constructor( - @ApplicationContext private val ctx: Context, -) : ToastAdapter { +class AndroidToastAdapter @Inject constructor(@ApplicationContext private val ctx: Context) : + ToastAdapter { override fun show(message: String) { Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show() } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/popup/ToastAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/popup/ToastAdapter.kt index 4cce00be35..5db5b1fdcc 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/popup/ToastAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/popup/ToastAdapter.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.system.popup - interface ToastAdapter { fun show(message: String) } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/power/AndroidPowerAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/power/AndroidPowerAdapter.kt index 10b6d5f619..bd9f0174ee 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/power/AndroidPowerAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/power/AndroidPowerAdapter.kt @@ -8,17 +8,16 @@ import android.os.BatteryManager import android.os.Build import androidx.core.content.ContextCompat import androidx.core.content.getSystemService -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow @Singleton -class AndroidPowerAdapter @Inject constructor( - @ApplicationContext private val context: Context -) : PowerAdapter { +class AndroidPowerAdapter @Inject constructor(@ApplicationContext private val context: Context) : + PowerAdapter { private val ctx: Context = context.applicationContext private val batteryManager: BatteryManager by lazy { ctx.getSystemService()!! } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/power/PowerAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/power/PowerAdapter.kt index ddaf4bb5f5..5d3dbef9a5 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/power/PowerAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/power/PowerAdapter.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.system.power import kotlinx.coroutines.flow.StateFlow - interface PowerAdapter { val isCharging: StateFlow } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/ringtones/RingtoneAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/ringtones/RingtoneAdapter.kt index 136fae35bc..798db85cce 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/ringtones/RingtoneAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/ringtones/RingtoneAdapter.kt @@ -13,7 +13,8 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class AndroidRingtoneAdapter @Inject constructor(@ApplicationContext private val ctx: Context) : RingtoneAdapter { +class AndroidRingtoneAdapter @Inject constructor(@ApplicationContext private val ctx: Context) : + RingtoneAdapter { private val ringtoneManager: RingtoneManager by lazy { RingtoneManager(ctx).apply { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shell/BaseShellAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/shell/BaseShellAdapter.kt index 289399166f..4d9652f0e7 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shell/BaseShellAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shell/BaseShellAdapter.kt @@ -5,6 +5,8 @@ import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.success +import java.io.IOException +import java.io.InterruptedIOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope @@ -13,58 +15,54 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withTimeoutOrNull -import java.io.IOException -import java.io.InterruptedIOException -abstract class BaseShellAdapter() : ShellAdapter { +abstract class BaseShellAdapter : ShellAdapter { abstract fun prepareCommand(command: String): Array - override suspend fun execute( - command: String, - timeoutMillis: Long, - ): KMResult = coroutineScope { - try { - val process = ProcessBuilder() - .command(*prepareCommand(command)) - // Redirect stderr to stdout - .redirectErrorStream(true) - .start() - - val stdoutReader = process.inputStream.bufferedReader() - var stdout = "" - + override suspend fun execute(command: String, timeoutMillis: Long): KMResult = + coroutineScope { try { - val readStdoutJob = launch(Dispatchers.IO) { - stdout = stdoutReader.readText() - } + val process = ProcessBuilder() + .command(*prepareCommand(command)) + // Redirect stderr to stdout + .redirectErrorStream(true) + .start() + + val stdoutReader = process.inputStream.bufferedReader() + var stdout = "" + + try { + val readStdoutJob = launch(Dispatchers.IO) { + stdout = stdoutReader.readText() + } - val exitCode = withTimeoutOrNull(timeoutMillis) { - // This is required so that the blocking process code is interrupted when - // the coroutine is cancelled by the timeout. - runInterruptible(Dispatchers.IO) { - process.waitFor() + val exitCode = withTimeoutOrNull(timeoutMillis) { + // This is required so that the blocking process code is interrupted when + // the coroutine is cancelled by the timeout. + runInterruptible(Dispatchers.IO) { + process.waitFor() + } } - } - readStdoutJob.cancel() + readStdoutJob.cancel() - if (exitCode == null) { - KMError.ShellCommandTimeout(timeoutMillis, stdout) - } else { - Success(ShellResult(stdout, exitCode)) + if (exitCode == null) { + KMError.ShellCommandTimeout(timeoutMillis, stdout) + } else { + Success(ShellResult(stdout, exitCode)) + } + } finally { + stdoutReader.close() + process.destroy() } - } finally { - stdoutReader.close() - process.destroy() + } catch (e: IOException) { + KMError.Exception(e) } - } catch (e: IOException) { - KMError.Exception(e) } - } override suspend fun executeWithStreamingOutput( command: String, - timeoutMillis: Long + timeoutMillis: Long, ): Flow> = callbackFlow { try { val process = ProcessBuilder() @@ -109,12 +107,10 @@ abstract class BaseShellAdapter() : ShellAdapter { } readStdoutJob.cancel() - } finally { process.destroy() stdoutReader.close() } - } catch (e: IOException) { trySend(KMError.Exception(e)) } finally { @@ -122,4 +118,4 @@ abstract class BaseShellAdapter() : ShellAdapter { awaitClose {} } } -} \ No newline at end of file +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt index a2090d3c7c..36515b0631 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt @@ -5,13 +5,10 @@ import io.github.sds100.keymapper.common.utils.KMResult import kotlinx.coroutines.flow.Flow interface ShellAdapter { - suspend fun execute( - command: String, - timeoutMillis: Long = 10000L - ): KMResult + suspend fun execute(command: String, timeoutMillis: Long = 10000L): KMResult suspend fun executeWithStreamingOutput( command: String, - timeoutMillis: Long + timeoutMillis: Long, ): Flow> } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shell/StandardShellAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/shell/StandardShellAdapter.kt index 5d11a0bb7b..209f4b6c44 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shell/StandardShellAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shell/StandardShellAdapter.kt @@ -9,4 +9,4 @@ class StandardShellAdapter @Inject constructor() : BaseShellAdapter() { // Execute through sh -c to properly handle multi-line commands and shell syntax return arrayOf("sh", "-c", command) } -} \ No newline at end of file +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuAdapter.kt index d3c1aabf02..7a54b549e6 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuAdapter.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.system.shizuku import kotlinx.coroutines.flow.StateFlow - interface ShizukuAdapter { val isInstalled: StateFlow val isStarted: StateFlow diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuAdapterImpl.kt b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuAdapterImpl.kt index b9db5d2541..c8149dca74 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuAdapterImpl.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuAdapterImpl.kt @@ -3,6 +3,8 @@ package io.github.sds100.keymapper.system.shizuku import io.github.sds100.keymapper.system.apps.PackageManagerAdapter import io.github.sds100.keymapper.system.apps.isAppInstalledFlow import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -10,8 +12,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import rikka.shizuku.Shizuku -import javax.inject.Inject -import javax.inject.Singleton @Singleton class ShizukuAdapterImpl @Inject constructor( diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt index 856129b345..7074fee4ab 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.system.shizuku import android.os.Build - object ShizukuUtils { const val SHIZUKU_PACKAGE = "moe.shizuku.privileged.api" diff --git a/system/src/main/java/io/github/sds100/keymapper/system/url/AndroidOpenUrlAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/url/AndroidOpenUrlAdapter.kt index 6c29a4cb36..fd3de33411 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/url/AndroidOpenUrlAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/url/AndroidOpenUrlAdapter.kt @@ -2,14 +2,13 @@ package io.github.sds100.keymapper.system.url import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.utils.KMResult import javax.inject.Inject import javax.inject.Singleton -import io.github.sds100.keymapper.common.utils.KMResult @Singleton -class AndroidOpenUrlAdapter @Inject constructor( - @ApplicationContext private val context: Context -) : OpenUrlAdapter { +class AndroidOpenUrlAdapter @Inject constructor(@ApplicationContext private val context: Context) : + OpenUrlAdapter { private val ctx = context.applicationContext diff --git a/system/src/main/java/io/github/sds100/keymapper/system/url/OpenUrlAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/url/OpenUrlAdapter.kt index 3fa4ec3c2e..f2977fae0f 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/url/OpenUrlAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/url/OpenUrlAdapter.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.system.url import io.github.sds100.keymapper.common.utils.KMResult - interface OpenUrlAdapter { fun openUrl(url: String): KMResult<*> } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/url/UrlUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/url/UrlUtils.kt index 68a5e58afe..6496c257c2 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/url/UrlUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/url/UrlUtils.kt @@ -8,8 +8,6 @@ import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.success - - object UrlUtils { fun openUrl(ctx: Context, url: String): KMResult<*> { Intent(Intent.ACTION_VIEW, url.toUri()).apply { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/vibrator/AndroidVibratorAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/vibrator/AndroidVibratorAdapter.kt index 51159df53f..30e14f32a6 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/vibrator/AndroidVibratorAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/vibrator/AndroidVibratorAdapter.kt @@ -11,9 +11,8 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class AndroidVibratorAdapter @Inject constructor( - @ApplicationContext private val context: Context -) : VibratorAdapter { +class AndroidVibratorAdapter @Inject constructor(@ApplicationContext private val context: Context) : + VibratorAdapter { private val vibrator: Vibrator? = context.getSystemService() override fun vibrate(duration: Long) { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/vibrator/VibratorAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/vibrator/VibratorAdapter.kt index 57f55f1559..ac717776b1 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/vibrator/VibratorAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/vibrator/VibratorAdapter.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.system.vibrator - interface VibratorAdapter { fun vibrate(duration: Long) } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt index 34376eab8a..ff871199ca 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt @@ -17,14 +17,14 @@ import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManage import io.github.sds100.keymapper.sysbridge.manager.isConnected import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import timber.log.Timber @Singleton class AndroidVolumeAdapter @Inject constructor( @ApplicationContext private val context: Context, - private val systemBridgeConnectionManager: SystemBridgeConnectionManager + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, ) : VolumeAdapter { private val ctx = context.applicationContext @@ -39,7 +39,7 @@ class AndroidVolumeAdapter @Inject constructor( val ringerModeSdk = if (systemBridgeConnectionManager.isConnected()) { SettingsUtils.getGlobalSetting( ctx, - Settings.Global.MODE_RINGER + Settings.Global.MODE_RINGER, ) ?: 0 } else { audioManager.ringerMode @@ -49,7 +49,9 @@ class AndroidVolumeAdapter @Inject constructor( AudioManager.RINGER_MODE_NORMAL -> RingerMode.NORMAL AudioManager.RINGER_MODE_VIBRATE -> RingerMode.VIBRATE AudioManager.RINGER_MODE_SILENT -> RingerMode.SILENT - else -> throw Exception("Don't know how to convert this ringer moder ${audioManager.ringerMode}") + else -> throw Exception( + "Don't know how to convert this ringer moder ${audioManager.ringerMode}", + ) } } @@ -99,7 +101,9 @@ class AndroidVolumeAdapter @Inject constructor( systemBridgeConnectionManager.run { systemBridge -> systemBridge.setRingerMode(sdkMode) }.otherwise { - Timber.e("Failed to set ringer mode with System Bridge, falling back to AudioManager") + Timber.e( + "Failed to set ringer mode with System Bridge, falling back to AudioManager", + ) audioManager.ringerMode = sdkMode Success(Unit) @@ -116,13 +120,11 @@ class AndroidVolumeAdapter @Inject constructor( override fun enableDndMode(dndMode: DndMode): KMResult<*> { notificationManager.setInterruptionFilter(dndMode.convert()) return Success(Unit) - } override fun disableDndMode(): KMResult<*> { notificationManager.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL) return Success(Unit) - } override fun isDndEnabled(): Boolean = diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt index 5460db38ec..2548902c5e 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.system.volume - enum class RingerMode { NORMAL, VIBRATE, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt index d6bf4f30ac..7bae48cc79 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.system.volume import io.github.sds100.keymapper.common.utils.KMResult - interface VolumeAdapter { val ringerMode: RingerMode diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeStream.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeStream.kt index fc243f7130..e7081a4fd3 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeStream.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeStream.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.system.volume - enum class VolumeStream { ALARM, DTMF, From c79f59cb328d1dc5f7385b800b6c2847acb33dc7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 14:45:03 +0100 Subject: [PATCH 08/37] fix: call getRootInActiveWindow() more safely in the accessibility service --- .../accessibility/BaseAccessibilityService.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index 1408163049..7517bdf437 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -13,6 +13,7 @@ import android.graphics.Point import android.os.Build import android.view.KeyEvent import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo import android.view.inputmethod.EditorInfo import androidx.annotation.RequiresApi import androidx.core.content.getSystemService @@ -64,7 +65,11 @@ abstract class BaseAccessibilityService : override val rootNode: AccessibilityNodeModel? get() { - return rootInActiveWindow?.toModel() + return try { + rootInActiveWindow?.toModel() + } catch (e: Exception) { + null + } } override val activeWindowPackageNames: List @@ -246,7 +251,15 @@ abstract class BaseAccessibilityService : event ?: return if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { - _activeWindowPackage.update { rootInActiveWindow?.packageName?.toString() } + // Catch exceptions because there is a crash report where + // getRootInActivityWindow() fails internally inside the AccessibilityService. + val rootNode: AccessibilityNodeInfo? = try { + rootInActiveWindow + } catch (_: Exception) { + null + } + + _activeWindowPackage.update { rootNode?.packageName?.toString() } } getController()?.onAccessibilityEvent(event) @@ -503,7 +516,7 @@ abstract class BaseAccessibilityService : findNode: (node: AccessibilityNodeModel) -> Boolean, performAction: (node: AccessibilityNodeModel) -> AccessibilityNodeAction?, ): KMResult<*> { - val node = rootInActiveWindow.findNodeRecursively { + val node = rootInActiveWindow?.findNodeRecursively { findNode(it.toModel()) } From 63a7a20315a10b47436fd0acefa3dea90b48b530 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 14:46:04 +0100 Subject: [PATCH 09/37] refactor: remove obsolete min sdk check --- .../accessibility/BaseAccessibilityService.kt | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index 7517bdf437..3acc3374a2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -469,47 +469,43 @@ abstract class BaseAccessibilityService : duration: Int, inputEventAction: InputEventAction, ): KMResult<*> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (fingerCount >= GestureDescription.getMaxStrokeCount()) { - return KMError.GestureStrokeCountTooHigh - } - if (duration >= GestureDescription.getMaxGestureDuration()) { - return KMError.GestureDurationTooHigh - } - - val gestureBuilder = GestureDescription.Builder() - val distributedPoints: List = - MathUtils.distributePointsOnCircle(Point(x, y), distance.toFloat() / 2, fingerCount) - - for (index in distributedPoints.indices) { - val p = Path() - if (pinchType == PinchScreenType.PINCH_IN) { - p.moveTo(x.toFloat(), y.toFloat()) - p.lineTo( - distributedPoints[index].x.toFloat(), - distributedPoints[index].y.toFloat(), - ) - } else { - p.moveTo( - distributedPoints[index].x.toFloat(), - distributedPoints[index].y.toFloat(), - ) - p.lineTo(x.toFloat(), y.toFloat()) - } - - gestureBuilder.addStroke(StrokeDescription(p, 0, duration.toLong())) - } + if (fingerCount >= GestureDescription.getMaxStrokeCount()) { + return KMError.GestureStrokeCountTooHigh + } + if (duration >= GestureDescription.getMaxGestureDuration()) { + return KMError.GestureDurationTooHigh + } - val success = dispatchGesture(gestureBuilder.build(), null, null) + val gestureBuilder = GestureDescription.Builder() + val distributedPoints: List = + MathUtils.distributePointsOnCircle(Point(x, y), distance.toFloat() / 2, fingerCount) - return if (success) { - Success(Unit) + for (index in distributedPoints.indices) { + val p = Path() + if (pinchType == PinchScreenType.PINCH_IN) { + p.moveTo(x.toFloat(), y.toFloat()) + p.lineTo( + distributedPoints[index].x.toFloat(), + distributedPoints[index].y.toFloat(), + ) } else { - KMError.FailedToDispatchGesture + p.moveTo( + distributedPoints[index].x.toFloat(), + distributedPoints[index].y.toFloat(), + ) + p.lineTo(x.toFloat(), y.toFloat()) } + + gestureBuilder.addStroke(StrokeDescription(p, 0, duration.toLong())) } - return KMError.SdkVersionTooLow(Build.VERSION_CODES.N) + val success = dispatchGesture(gestureBuilder.build(), null, null) + + return if (success) { + Success(Unit) + } else { + KMError.FailedToDispatchGesture + } } override fun performActionOnNode( From d4bfb6cc0103eed6096335bb05de34abbfe0451e Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 14:49:18 +0100 Subject: [PATCH 10/37] fix: check if subtype history is null in AndroidInputMethodAdapter.kt --- .../system/inputmethod/AndroidInputMethodAdapter.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt index 11d23b6b02..e1c6b70e18 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt @@ -128,7 +128,9 @@ class AndroidInputMethodAdapter @Inject constructor( * :com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME;1891618174 */ private fun getImeHistory(): List { - val ids = getSubtypeHistoryString(ctx) + val subtypeString = getSubtypeHistoryString(ctx) ?: return emptyList() + + val ids = subtypeString .split(':') .map { it.split(';')[0] } @@ -156,7 +158,7 @@ class AndroidInputMethodAdapter @Inject constructor( } } - private fun getSubtypeHistoryString(ctx: Context): String = Settings.Secure.getString( + private fun getSubtypeHistoryString(ctx: Context): String? = Settings.Secure.getString( ctx.contentResolver, SETTINGS_SECURE_SUBTYPE_HISTORY_KEY, ) From 01c3560813bd026e0f6b99583195b2f0a3c05a28 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 15:06:59 +0100 Subject: [PATCH 11/37] fix: send key event to relay service on IO thread --- .../inputmethod/ImeInputEventInjector.kt | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt index 1f099f6756..41272fea50 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt @@ -8,6 +8,8 @@ import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import timber.log.Timber /** @@ -23,7 +25,7 @@ class ImeInputEventInjectorImpl @Inject constructor( private const val CALLBACK_ID_INPUT_METHOD = "input_method" } - override fun inputKeyEvent(event: InjectKeyEventModel) { + override suspend fun inputKeyEvent(event: InjectKeyEventModel) { Timber.d("Inject key event with input method $event") val imePackageName = inputMethodAdapter.chosenIme.value?.packageName @@ -33,11 +35,15 @@ class ImeInputEventInjectorImpl @Inject constructor( return } - keyEventRelayService.sendKeyEvent( - event.toAndroidKeyEvent(), - imePackageName, - CALLBACK_ID_INPUT_METHOD, - ) + // This IPC call can potentially take a long time so do not block the main thread + // and cause an ANR. + withContext(Dispatchers.IO) { + keyEventRelayService.sendKeyEvent( + event.toAndroidKeyEvent(), + imePackageName, + CALLBACK_ID_INPUT_METHOD, + ) + } } override fun inputText(text: String) { @@ -102,5 +108,5 @@ class ImeInputEventInjectorImpl @Inject constructor( interface ImeInputEventInjector { fun inputText(text: String) - fun inputKeyEvent(event: InjectKeyEventModel) + suspend fun inputKeyEvent(event: InjectKeyEventModel) } From ff09dd1604b72e8a6c686a27822c88970c1fe070 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 15:11:42 +0100 Subject: [PATCH 12/37] refactor: remove obsolete min sdk check --- .../swipescreen/SwipePickDisplayCoordinateViewModel.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/swipescreen/SwipePickDisplayCoordinateViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/swipescreen/SwipePickDisplayCoordinateViewModel.kt index da694457b9..e4fa5cb8ce 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/swipescreen/SwipePickDisplayCoordinateViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/swipescreen/SwipePickDisplayCoordinateViewModel.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.base.actions.swipescreen import android.accessibilityservice.GestureDescription import android.graphics.Bitmap import android.graphics.Point -import android.os.Build import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -96,9 +95,7 @@ class SwipePickDisplayCoordinateViewModel @Inject constructor( var maxFingerCount = 10 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - maxFingerCount = GestureDescription.getMaxStrokeCount() - } + maxFingerCount = GestureDescription.getMaxStrokeCount() if (count > maxFingerCount) { return@map resourceProvider.getString( From 63f67ff516ee2e2eb1f37b110480590b7ecaacaf Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 15:11:48 +0100 Subject: [PATCH 13/37] style: invert if statement --- .../github/sds100/keymapper/base/input/EvdevHandleCache.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt index 5db30e4278..aa592e491e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt @@ -55,10 +55,10 @@ class EvdevHandleCache @Inject constructor( devicesAdapter.connectedInputDevices, systemBridgeConnectionManager.connectionState, ) { _, connectionState -> - if (connectionState !is SystemBridgeConnectionState.Connected) { - devicesByPath.value = emptyMap() - } else { + if (connectionState is SystemBridgeConnectionState.Connected) { invalidate() + } else { + devicesByPath.value = emptyMap() } }.collect() } From 3a3034c039c8ad734ee0ca46204c8f760389de8c Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 15:34:19 +0100 Subject: [PATCH 14/37] fix: use separate thread for dispatching gestures in accessibility service --- .../accessibility/BaseAccessibilityService.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index 3acc3374a2..24b064b4ad 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -11,6 +11,8 @@ import android.content.res.Configuration import android.graphics.Path import android.graphics.Point import android.os.Build +import android.os.Handler +import android.os.HandlerThread import android.view.KeyEvent import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo @@ -156,6 +158,12 @@ abstract class BaseAccessibilityService : abstract fun getController(): BaseAccessibilityServiceController? + /** + * Use a separate thread for dispatching gestures so they do not cause an ANR. + */ + private val gestureHandlerThread: HandlerThread = HandlerThread("gesture_thread") + private var gestureHandler: Handler? = null + override fun onCreate() { super.onCreate() Timber.i("Accessibility service: onCreate") @@ -210,6 +218,9 @@ abstract class BaseAccessibilityService : fingerprintGestureCallback?.let { fingerprintGestureController.registerFingerprintGestureCallback(it, null) } + + gestureHandlerThread.start() + gestureHandler = Handler(gestureHandlerThread.looper) } override fun onUnbind(intent: Intent?): Boolean { @@ -222,6 +233,8 @@ abstract class BaseAccessibilityService : override fun onDestroy() { lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + gestureHandlerThread.quit() + fingerprintGestureController .unregisterFingerprintGestureCallback(fingerprintGestureCallback) @@ -352,7 +365,7 @@ abstract class BaseAccessibilityService : addStroke(it) }.build() - val success = dispatchGesture(gestureDescription, null, null) + val success = dispatchGesture(gestureDescription, null, gestureHandler) return if (success) { Success(Unit) @@ -451,7 +464,7 @@ abstract class BaseAccessibilityService : } } - val success = dispatchGesture(gestureBuilder.build(), null, null) + val success = dispatchGesture(gestureBuilder.build(), null, gestureHandler) return if (success) { Success(Unit) @@ -499,7 +512,7 @@ abstract class BaseAccessibilityService : gestureBuilder.addStroke(StrokeDescription(p, 0, duration.toLong())) } - val success = dispatchGesture(gestureBuilder.build(), null, null) + val success = dispatchGesture(gestureBuilder.build(), null, gestureHandler) return if (success) { Success(Unit) From b67e6f80fbf67dfe017179c6961bc5d894f6be54 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 2 Nov 2025 16:08:14 +0100 Subject: [PATCH 15/37] shorten strings for swipe/pinch actions --- base/src/main/res/values/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 188a2d3f59..d9cd95c2b8 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -100,10 +100,10 @@ Open %s Tap screen (%d, %d) Tap screen (%s) - Swipe with %d finger(s) from coordinates %d/%d to %d/%d in %dms - Swipe with %d finger(s) from coordinates %d/%d to %d/%d in %dms (%s) - %s with %d finger(s) on coordinates %d/%d with a pinch distance of %dpx in %dms - %s with %d finger(s) on coordinates %d/%d to with a pinch distance of %dpx %dms (%s) + Swipe with %d finger(s) from (%d,%d) to (%d,%d) in %dms + Swipe with %d finger(s) from (%d,%d) to (%d,%d) in %dms (%s) + %s with %d finger(s) on (%d,%d) with a pinch distance of %dpx in %dms + %s with %d finger(s) on (%d,%d) to with a pinch distance of %dpx %dms (%s) Call %s Play sound: %s Play unknown sound From 4ea72e30fbc1f0239373edfedcdf7b45ee3660b6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 4 Nov 2025 19:30:06 +0100 Subject: [PATCH 16/37] remove unused method in OnboardingUseCase.kt --- .../sds100/keymapper/base/onboarding/OnboardingUseCase.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt index fef578271f..b3b60ddc60 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt @@ -64,8 +64,6 @@ class OnboardingUseCaseImpl @Inject constructor( set(Keys.lastInstalledVersionCodeBackground, buildConfigProvider.versionCode) } - override fun isTvDevice(): Boolean = leanbackAdapter.isTvDevice() - override val promptForShizukuPermission: Flow = combine( settingsRepository.get(Keys.shownShizukuPermissionPrompt), shizukuAdapter.isInstalled, @@ -110,8 +108,6 @@ class OnboardingUseCaseImpl @Inject constructor( interface OnboardingUseCase { var shownAppIntro: Boolean - fun isTvDevice(): Boolean - val showFloatingButtonFeatureNotification: Flow fun showedFloatingButtonFeatureNotification() var approvedFloatingButtonFeaturePrompt: Boolean From b636e620e7b9ecdcbe77097f66afc31d048944c8 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 4 Nov 2025 20:16:04 +0100 Subject: [PATCH 17/37] #1875 show trigger error to migrate screen off key maps --- .../keymapper/base/home/KeyMapListScreen.kt | 3 +++ .../base/keymaps/DisplayKeyMapUseCase.kt | 3 ++- .../trigger/BaseConfigTriggerViewModel.kt | 19 +++++++++++++++ .../base/trigger/KeyEventTriggerKey.kt | 3 ++- .../sds100/keymapper/base/trigger/Trigger.kt | 20 ++++++++++++++++ .../keymapper/base/trigger/TriggerError.kt | 2 ++ .../base/trigger/TriggerErrorSnapshot.kt | 23 +++++++++++++++++++ .../base/trigger/TriggerKeyListItem.kt | 3 +++ base/src/main/res/values/strings.xml | 5 ++++ 9 files changed, 79 insertions(+), 2 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt index 9d8a47a83d..1ed0b69eaa 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt @@ -506,6 +506,9 @@ private fun getTriggerErrorMessage(error: TriggerError): String { TriggerError.EVDEV_DEVICE_NOT_FOUND -> stringResource( R.string.trigger_error_evdev_device_not_found, ) + TriggerError.MIGRATE_SCREEN_OFF_TRIGGER -> stringResource( + R.string.trigger_error_migrate_screen_off_key_map, + ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt index 8cda19f4fa..4d23b2b1cd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt @@ -42,7 +42,6 @@ import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuUtils -import javax.inject.Inject import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -54,6 +53,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withTimeout +import javax.inject.Inject @ViewModelScoped class DisplayKeyMapUseCaseImpl @Inject constructor( @@ -181,6 +181,7 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( TriggerError.EVDEV_DEVICE_NOT_FOUND, TriggerError.FLOATING_BUTTON_DELETED, TriggerError.SYSTEM_BRIDGE_UNSUPPORTED, + TriggerError.MIGRATE_SCREEN_OFF_TRIGGER, -> {} } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 439ed29438..9ea19d39e7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -23,10 +23,13 @@ import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem +import io.github.sds100.keymapper.base.utils.ui.DialogModel import io.github.sds100.keymapper.base.utils.ui.DialogProvider +import io.github.sds100.keymapper.base.utils.ui.DialogResponse import io.github.sds100.keymapper.base.utils.ui.LinkType import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.ViewModelHelper +import io.github.sds100.keymapper.base.utils.ui.showDialog import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.AccessibilityServiceError import io.github.sds100.keymapper.common.utils.InputDeviceUtils @@ -526,6 +529,22 @@ abstract class BaseConfigTriggerViewModel( showTriggerSetup(TriggerSetupShortcut.GAMEPAD) } + TriggerError.MIGRATE_SCREEN_OFF_TRIGGER -> { + val dialog = DialogModel.Alert( + title = getString(R.string.dialog_title_migrate_screen_off_key_map), + message = getString(R.string.dialog_message_migrate_screen_off_key_map), + positiveButtonText = + getString(R.string.dialog_button_migrate_screen_off_key_map), + negativeButtonText = getString(R.string.neg_cancel), + ) + + val response = showDialog("migrate_screen_off", dialog) + + if (response == DialogResponse.POSITIVE) { + showTriggerSetup(TriggerSetupShortcut.OTHER, forceProMode = true) + } + } + else -> displayKeyMap.fixTriggerError(error) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt index ebd8104ea8..c5b5f20d9f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt @@ -4,6 +4,7 @@ import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.TriggerEntity import io.github.sds100.keymapper.data.entities.TriggerKeyEntity import java.util.UUID import kotlinx.serialization.Serializable @@ -74,7 +75,7 @@ data class KeyEventTriggerKey( entity.flags.hasFlag(KeyEventTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) val detectWithScancode = - entity.flags.hasFlag(KeyEventTriggerKeyEntity.FLAG_DETECT_WITH_SCAN_CODE) + entity.flags.hasFlag(TriggerEntity.TRIGGER_FLAG_VIBRATE) return KeyEventTriggerKey( uid = entity.uid, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt index 303993d860..4150648257 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt @@ -28,6 +28,15 @@ data class Trigger( val sequenceTriggerTimeout: Int? = null, val triggerFromOtherApps: Boolean = false, val showToast: Boolean = false, + + /** + * This is true if the user turned on the "screen off" option in versions prior to v4.0.0. This + * option has now been removed and replaced with the system bridge but they will have to + * record their keys again and create an EvdevTriggerKey. We can not handle this migration + * automatically because we could grab the wrong evdev device or if there is a bug + * severely mess up their device without them knowing the cause. + */ + val legacyDetectScreenOff: Boolean = false, ) { fun isVibrateAllowed(): Boolean = true @@ -102,6 +111,7 @@ object TriggerEntityMapper { else -> TriggerMode.Undefined } + @Suppress("DEPRECATION") return Trigger( keys = keys, mode = mode, @@ -127,6 +137,9 @@ object TriggerEntityMapper { triggerFromOtherApps = entity.flags.hasFlag(TriggerEntity.TRIGGER_FLAG_FROM_OTHER_APPS), showToast = entity.flags.hasFlag(TriggerEntity.TRIGGER_FLAG_SHOW_TOAST), + legacyDetectScreenOff = entity.flags.hasFlag( + TriggerEntity.TRIGGER_FLAG_SCREEN_OFF_TRIGGERS, + ), ) } @@ -195,6 +208,13 @@ object TriggerEntityMapper { flags = flags.withFlag(TriggerEntity.TRIGGER_FLAG_SHOW_TOAST) } + // Still persist this so that any errors do not disappear when the key map is saved + // for whatever reason. + if (trigger.legacyDetectScreenOff) { + @Suppress("DEPRECATION") + flags = flags.withFlag(TriggerEntity.TRIGGER_FLAG_SCREEN_OFF_TRIGGERS) + } + val keys = trigger.keys.map { key -> when (key) { is AssistantTriggerKey -> AssistantTriggerKey.toEntity(key) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt index eea5a79fc6..29c38e5609 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt @@ -24,4 +24,6 @@ enum class TriggerError(val isFixable: Boolean) { SYSTEM_BRIDGE_DISCONNECTED(isFixable = true), EVDEV_DEVICE_NOT_FOUND(isFixable = false), + + MIGRATE_SCREEN_OFF_TRIGGER(isFixable = true), } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt index 86335276b2..71760e8051 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt @@ -35,9 +35,32 @@ data class TriggerErrorSnapshot( KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, ) + + fun isScreenOffTriggerMigrationRequired(trigger: Trigger, key: TriggerKey): Boolean = + trigger.legacyDetectScreenOff && + key is KeyEventTriggerKey && + key.keyCode in LEGACY_SCREEN_OFF_KEY_CODES + + /** + * These are the key codes that were detected with the getevent command prior to v4.0.0. + */ + val LEGACY_SCREEN_OFF_KEY_CODES: Set = setOf( + KeyEvent.KEYCODE_VOLUME_DOWN, + KeyEvent.KEYCODE_VOLUME_UP, + KeyEvent.KEYCODE_HEADSETHOOK, + KeyEvent.KEYCODE_FOCUS, + KeyEvent.KEYCODE_CAMERA, + KeyEvent.KEYCODE_MENU, + KeyEvent.KEYCODE_ASSIST, + KeyEvent.KEYCODE_SEARCH, + ) } fun getTriggerError(keyMap: KeyMap, key: TriggerKey): TriggerError? { + if (isScreenOffTriggerMigrationRequired(keyMap.trigger, key)) { + return TriggerError.MIGRATE_SCREEN_OFF_TRIGGER + } + purchases.onSuccess { purchases -> if (key is AssistantTriggerKey && !purchases.contains(ProductId.ASSISTANT_TRIGGER)) { return TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index d04962ecb5..3fb455de52 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -305,6 +305,9 @@ private fun getErrorMessage(error: TriggerError): String { TriggerError.EVDEV_DEVICE_NOT_FOUND -> stringResource( R.string.trigger_error_evdev_device_not_found, ) + TriggerError.MIGRATE_SCREEN_OFF_TRIGGER -> stringResource( + R.string.trigger_error_migrate_screen_off_key_map, + ) } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index d9cd95c2b8..3fa604f132 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -80,6 +80,7 @@ PRO mode is unsupported on this Android version PRO mode not started! Trigger device not connected! + Migrate this screen off trigger Your key maps will stop working randomly! Your key maps are paused! @@ -476,6 +477,10 @@ Turn on notifications Some actions and options need this permission to work. You can also get notified when there is important news about the app. + Migrate this screen off trigger + We\'re sorry for the disruption. We\'ve introduced a *free* feature called PRO mode that replaces the old screen off remapping option. This feature is much more reliable and unlocks remapping the power button too.\n\nDue to the way it works you will need to record the trigger again. + Proceed + Done Kill Guide From a5a140e6d6ece7a83610f2a73bf16cb3a874d965 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 4 Nov 2025 20:29:15 +0100 Subject: [PATCH 18/37] fix: only autostart system bridge with Shizuku if Shizuku permission is granted. Otherwise fallback to method with Wireless Debugging and WRITE_SECURE_SETTINGS permission --- CHANGELOG.md | 8 ++++++++ .../base/promode/SystemBridgeAutoStarter.kt | 12 ++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adee47d591..274dea5cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [4.0.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.02) + +#### TO BE RELEASED + +## Fixed + +- Only autostart PRO mode with Shizuku if Shizuku permission is granted. Otherwise fallback to method with Wireless Debugging and WRITE_SECURE_SETTINGS permission + ## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01) #### 01 November 2025 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt index 9d73d14af0..9c949825bf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt @@ -76,8 +76,16 @@ class SystemBridgeAutoStarter @Inject constructor( if (isRooted) { flowOf(AutoStartType.ROOT) } else { - shizukuAdapter.isStarted.flatMapLatest { isShizukuStarted -> - if (isShizukuStarted) { + val useShizukuFlow = + combine( + shizukuAdapter.isStarted, + permissionAdapter.isGrantedFlow(Permission.SHIZUKU), + ) { isStarted, isGranted -> + isStarted && isGranted + } + + useShizukuFlow.flatMapLatest { useShizuku -> + if (useShizuku) { flowOf(AutoStartType.SHIZUKU) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val isAdbAutoStartAllowed = combine( From 4047439118c6e236a7e961d362f4f24227d7524a Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 4 Nov 2025 21:04:04 +0100 Subject: [PATCH 19/37] remove floating buttons feature notification --- .../base/onboarding/OnboardingUseCase.kt | 20 ------------- .../notifications/NotificationController.kt | 30 ------------------- .../io/github/sds100/keymapper/data/Keys.kt | 4 +-- 3 files changed, 2 insertions(+), 52 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt index b3b60ddc60..49e4c2d886 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt @@ -44,26 +44,6 @@ class OnboardingUseCaseImpl @Inject constructor( readText() } - override var approvedFloatingButtonFeaturePrompt by PrefDelegate( - Keys.approvedFloatingButtonFeaturePrompt, - false, - ) - - /** - * Show only when they *upgrade* to the new version and after they've - * completed the app intro, which asks them whether they want to receive notifications. - */ - override val showFloatingButtonFeatureNotification: Flow = combine( - get(Keys.lastInstalledVersionCodeBackground).map { it ?: -1 }, - get(Keys.approvedFloatingButtonFeaturePrompt).map { it ?: false }, - ) { oldVersionCode, approvedPrompt -> - oldVersionCode < VersionHelper.FLOATING_BUTTON_MIN_VERSION && !approvedPrompt - } - - override fun showedFloatingButtonFeatureNotification() { - set(Keys.lastInstalledVersionCodeBackground, buildConfigProvider.versionCode) - } - override val promptForShizukuPermission: Flow = combine( settingsRepository.get(Keys.shownShizukuPermissionPrompt), shizukuAdapter.isInstalled, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index 2c9fcc417b..eb3a646f0b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -124,20 +124,6 @@ class NotificationController @Inject constructor( } }.flowOn(dispatchers.default()).launchIn(coroutineScope) - coroutineScope.launch(dispatchers.default()) { - // suspend until the notification should be shown. - onboardingUseCase.showFloatingButtonFeatureNotification.first { it } - - manageNotifications.show(floatingButtonFeatureNotification()) - - // Only save that the notification is shown if the app has - // permissions to show notifications so that it is shown - // the next time permission is granted. - if (manageNotifications.isPermissionGranted()) { - onboardingUseCase.showedFloatingButtonFeatureNotification() - } - } - hideInputMethod.onHiddenChange.onEach { isHidden -> manageNotifications.createChannel( NotificationChannelModel( @@ -391,22 +377,6 @@ class NotificationController @Inject constructor( priority = NotificationCompat.PRIORITY_LOW, ) - private fun floatingButtonFeatureNotification(): NotificationModel = NotificationModel( - id = ID_FEATURE_FLOATING_BUTTONS, - channel = CHANNEL_NEW_FEATURES, - title = getString(R.string.notification_floating_buttons_feature_title), - text = getString(R.string.notification_floating_buttons_feature_text), - icon = R.drawable.outline_bubble_chart_24, - onClickAction = KMNotificationAction.Activity.MainActivity( - BaseMainActivity.ACTION_USE_FLOATING_BUTTONS, - ), - priority = NotificationCompat.PRIORITY_LOW, - autoCancel = true, - onGoing = false, - showOnLockscreen = false, - bigTextStyle = true, - ) - private fun showSystemBridgeStartedNotification() { val model = NotificationModel( id = ID_SYSTEM_BRIDGE_STATUS, diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 013df385f9..17ed321225 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -46,8 +46,8 @@ object Keys { // val approvedAssistantTriggerFeaturePrompt = // booleanPreferencesKey("pref_approved_assistant_trigger_feature_prompt") - val approvedFloatingButtonFeaturePrompt = - booleanPreferencesKey("pref_approved_floating_button_feature_prompt") +// val approvedFloatingButtonFeaturePrompt = +// booleanPreferencesKey("pref_approved_floating_button_feature_prompt") val shownParallelTriggerOrderExplanation = booleanPreferencesKey("key_shown_parallel_trigger_order_warning") From cfbdc1dd563dbd2ecbc4b11846640d31c782c857 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 4 Nov 2025 21:05:50 +0100 Subject: [PATCH 20/37] #1875 feat: show notification warning the user to migrate their screen off key maps --- .../sds100/keymapper/base/BaseMainActivity.kt | 1 + .../base/onboarding/OnboardingUseCase.kt | 42 +++- .../notifications/NotificationController.kt | 19 ++ base/src/main/res/values/strings.xml | 6 +- .../base/trigger/TriggerErrorSnapshotTest.kt | 206 ++++++++++++++++++ .../io/github/sds100/keymapper/data/Keys.kt | 3 + 6 files changed, 267 insertions(+), 10 deletions(-) create mode 100644 base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshotTest.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 812ca3b3fb..9b19ba1328 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -204,6 +204,7 @@ abstract class BaseMainActivity : AppCompatActivity() { suAdapter.invalidateIsRooted() systemBridgeSetupController.invalidateSettings() networkAdapter.invalidateState() + onboardingUseCase.handledMigrateScreenOffKeyMapsNotification() } override fun onDestroy() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt index 49e4c2d886..d29de0b78f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt @@ -1,14 +1,15 @@ package io.github.sds100.keymapper.base.onboarding import androidx.datastore.preferences.core.Preferences -import io.github.sds100.keymapper.base.utils.VersionHelper +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.keymaps.KeyMapEntityMapper +import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.data.utils.PrefDelegate import io.github.sds100.keymapper.system.files.FileAdapter -import io.github.sds100.keymapper.system.leanback.LeanbackAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter @@ -16,13 +17,13 @@ import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @Singleton class OnboardingUseCaseImpl @Inject constructor( private val settingsRepository: PreferenceRepository, private val fileAdapter: FileAdapter, - private val leanbackAdapter: LeanbackAdapter, private val shizukuAdapter: ShizukuAdapter, private val permissionAdapter: PermissionAdapter, private val keyMapRepository: KeyMapRepository, @@ -83,15 +84,39 @@ class OnboardingUseCaseImpl @Inject constructor( OnboardingTapTarget.CREATE_KEY_MAP -> Keys.shownTapTargetCreateKeyMap } } + + override val showMigrateScreenOffKeyMapsNotification: Flow = + get(Keys.handledMigrateScreenOffKeyMapsNotification).map { isHandled -> + if (isHandled == true) { + return@map false + } + + val keyMaps = keyMapRepository.getAll() + .first() + .map { keyMap -> KeyMapEntityMapper.fromEntity(keyMap, emptyList()) } + + isScreenOffTriggerMigrationRequired(keyMaps) + } + + override fun handledMigrateScreenOffKeyMapsNotification() = + set(Keys.handledMigrateScreenOffKeyMapsNotification, true) + + private fun isScreenOffTriggerMigrationRequired(keyMapList: List): Boolean { + for (keyMap in keyMapList) { + for (key in keyMap.trigger.keys) { + if (TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(keyMap.trigger, key)) { + return true + } + } + } + + return false + } } interface OnboardingUseCase { var shownAppIntro: Boolean - val showFloatingButtonFeatureNotification: Flow - fun showedFloatingButtonFeatureNotification() - var approvedFloatingButtonFeaturePrompt: Boolean - val showWhatsNew: Flow fun showedWhatsNew() fun getWhatsNewText(): String @@ -105,4 +130,7 @@ interface OnboardingUseCase { fun showTapTarget(tapTarget: OnboardingTapTarget): Flow fun completedTapTarget(tapTarget: OnboardingTapTarget) + + val showMigrateScreenOffKeyMapsNotification: Flow + fun handledMigrateScreenOffKeyMapsNotification() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index eb3a646f0b..37ffa5125a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -60,6 +60,7 @@ class NotificationController @Inject constructor( // private const val ID_FEATURE_ASSISTANT_TRIGGER = 900 private const val ID_FEATURE_FLOATING_BUTTONS = 901 + private const val ID_MIGRATE_SCREEN_OFF_KEY_MAPS = 902 const val CHANNEL_TOGGLE_KEY_MAPS = "channel_toggle_remaps" @@ -70,6 +71,7 @@ class NotificationController @Inject constructor( const val CHANNEL_TOGGLE_KEYBOARD = "channel_toggle_keymapper_keyboard" const val CHANNEL_NEW_FEATURES = "channel_new_features" const val CHANNEL_SETUP_ASSISTANT = "channel_setup_assistant" + const val CHANNEL_VERSION_MIGRATION = "channel_version_migration" @Deprecated("Removed in 2.0. This channel shouldn't exist") private const val CHANNEL_ID_WARNINGS = "channel_warnings" @@ -176,6 +178,23 @@ class NotificationController @Inject constructor( } } } + + coroutineScope.launch { + if (onboardingUseCase.showMigrateScreenOffKeyMapsNotification.first()) { + manageNotifications.show( + NotificationModel( + id = ID_MIGRATE_SCREEN_OFF_KEY_MAPS, + channel = CHANNEL_NEW_FEATURES, + title = getString(R.string.notification_migrate_screen_off_key_map_title), + text = getString(R.string.notification_migrate_screen_off_key_map_text), + icon = R.drawable.ic_baseline_warning_24, + onClickAction = KMNotificationAction.Activity.MainActivity(), + showOnLockscreen = true, + onGoing = false, + ), + ) + } + } } fun onOpenApp() { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 3fa604f132..c21c3a5203 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -478,7 +478,7 @@ Some actions and options need this permission to work. You can also get notified when there is important news about the app. Migrate this screen off trigger - We\'re sorry for the disruption. We\'ve introduced a *free* feature called PRO mode that replaces the old screen off remapping option. This feature is much more reliable and unlocks remapping the power button too.\n\nDue to the way it works you will need to record the trigger again. + We\'re sorry for the disruption. We\'ve introduced a *free* feature called PRO mode that replaces the old screen off remapping option. This feature is much more reliable and unlocks remapping the power button too.\n\nDue to the way it works you will need to record this trigger again with PRO mode. Proceed Done @@ -546,6 +546,8 @@ Tap \'toggle\' to switch to and from the Key Mapper Input Method. Toggle + Your screen off key maps need attention! + We\'re sorry for the disruption but you will need to record your screen off key maps again. @@ -1389,8 +1391,6 @@ Configure button Edit layout Requires Android 11 or newer. - Don’t have enough buttons? Now you can make your own! - Floating buttons display over the apps you want. They work just like real buttons, and you can place, style, and map them however you like. Trigger diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshotTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshotTest.kt new file mode 100644 index 0000000000..3466ea36fc --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshotTest.kt @@ -0,0 +1,206 @@ +package io.github.sds100.keymapper.base.trigger + +import android.view.KeyEvent +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.purchasing.ProductId +import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.system.inputevents.Scancode +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner + +@ExperimentalCoroutinesApi +@RunWith(MockitoJUnitRunner::class) +class TriggerErrorSnapshotTest { + + @Test + fun `do not show error for a key event trigger that did not support screen off remapping to migrate with pro mode`() { + val trigger = Trigger( + legacyDetectScreenOff = true, + keys = listOf( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + clickType = ClickType.SHORT_PRESS, + device = KeyEventTriggerDevice.Internal, + ), + ), + ) + + assertThat( + TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(trigger, trigger.keys[0]), + `is`(false), + ) + } + + @Test + fun `do not show error for an evdev trigger to migrate with pro mode`() { + val trigger = Trigger( + legacyDetectScreenOff = true, + keys = listOf( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = EvdevDeviceInfo("gpio_keys", 0, 0, 0), + ), + ), + ) + + assertThat( + TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(trigger, trigger.keys[0]), + `is`(false), + ) + } + + @Test + fun `do not show error for a volume trigger that does not have screen off remap enabled`() { + val trigger = Trigger( + legacyDetectScreenOff = false, + keys = listOf( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + clickType = ClickType.SHORT_PRESS, + device = KeyEventTriggerDevice.Internal, + ), + ), + ) + + assertThat( + TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(trigger, trigger.keys[0]), + `is`(false), + ) + } + + @Test + fun `show error for legacy screen off volume triggers with also an evdev key to migrate with pro mode`() { + val trigger = Trigger( + legacyDetectScreenOff = true, + keys = listOf( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = EvdevDeviceInfo("gpio_keys", 0, 0, 0), + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + clickType = ClickType.SHORT_PRESS, + device = KeyEventTriggerDevice.Internal, + ), + ), + ) + + assertThat( + TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(trigger, trigger.keys[0]), + `is`(false), + ) + + assertThat( + TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(trigger, trigger.keys[1]), + `is`(true), + ) + } + + @Test + fun `show error for legacy screen off volume triggers with multiple keys to migrate with pro mode`() { + val trigger = Trigger( + legacyDetectScreenOff = true, + keys = listOf( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + clickType = ClickType.SHORT_PRESS, + device = KeyEventTriggerDevice.Internal, + ), + FingerprintTriggerKey( + type = FingerprintGestureType.SWIPE_DOWN, + clickType = ClickType.LONG_PRESS, + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + clickType = ClickType.SHORT_PRESS, + device = KeyEventTriggerDevice.Internal, + ), + ), + ) + + assertThat( + TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(trigger, trigger.keys[0]), + `is`(false), + ) + + assertThat( + TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(trigger, trigger.keys[1]), + `is`(false), + ) + + assertThat( + TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(trigger, trigger.keys[2]), + `is`(true), + ) + } + + @Test + fun `show error for legacy screen off volume triggers to migrate with pro mode if pro mode is enabled`() { + val trigger = Trigger( + legacyDetectScreenOff = true, + keys = listOf( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + clickType = ClickType.SHORT_PRESS, + device = KeyEventTriggerDevice.Internal, + ), + ), + ) + + assertThat( + TriggerErrorSnapshot.isScreenOffTriggerMigrationRequired(trigger, trigger.keys[0]), + `is`(true), + ) + } + + @Test + fun `show error for legacy screen off volume triggers to migrate with pro mode if pro mode is disabled`() { + val trigger = Trigger( + legacyDetectScreenOff = true, + keys = listOf( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + clickType = ClickType.SHORT_PRESS, + device = KeyEventTriggerDevice.Internal, + ), + ), + ) + + val keyMap = KeyMap(trigger = trigger) + + val error = createTriggerErrorSnapshot( + isSystemBridgeConnected = false, + ).getTriggerError(keyMap, trigger.keys[0]) + assertThat(error, `is`(TriggerError.MIGRATE_SCREEN_OFF_TRIGGER)) + } + + private fun createTriggerErrorSnapshot( + isKeyMapperImeChosen: Boolean = true, + isDndAccessGranted: Boolean = true, + isRootGranted: Boolean = false, + purchases: KMResult> = Success(emptySet()), + showDpadImeSetupError: Boolean = false, + isSystemBridgeConnected: Boolean? = true, + evdevDevices: List? = emptyList(), + ): TriggerErrorSnapshot { + return TriggerErrorSnapshot( + isKeyMapperImeChosen = isKeyMapperImeChosen, + isDndAccessGranted = isDndAccessGranted, + isRootGranted = isRootGranted, + purchases = purchases, + showDpadImeSetupError = showDpadImeSetupError, + isSystemBridgeConnected = isSystemBridgeConnected, + evdevDevices = evdevDevices, + ) + } +} diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 17ed321225..f13bec696e 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -149,4 +149,7 @@ object Keys { * to 4.0 on a rooted device. */ val handledRootToProModeUpgrade = booleanPreferencesKey("key_handled_root_to_pro_mode_upgrade") + + val handledMigrateScreenOffKeyMapsNotification = + booleanPreferencesKey("key_handled_migrate_screen_off_key_maps_notification") } From 5e05fa7f7b1b50b2efc56f4f8670831e673d1354 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 13:00:03 +0100 Subject: [PATCH 21/37] style: reformat --- .../sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt index 4d23b2b1cd..74f7a31df3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt @@ -42,6 +42,7 @@ import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuUtils +import javax.inject.Inject import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -53,7 +54,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withTimeout -import javax.inject.Inject @ViewModelScoped class DisplayKeyMapUseCaseImpl @Inject constructor( From 1663e3ed4b1198a2db02d14a88c87987981c3598 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 13:09:41 +0100 Subject: [PATCH 22/37] chore: update whats new --- base/src/main/assets/whats-new.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/assets/whats-new.txt b/base/src/main/assets/whats-new.txt index 2e842ec8ee..d170dd02d8 100644 --- a/base/src/main/assets/whats-new.txt +++ b/base/src/main/assets/whats-new.txt @@ -1,5 +1,5 @@ ✨ Screen-off remapping -You can now remap buttons when the screen is off (including the power button) for free with PRO mode. +You can now remap ALL buttons when the screen is off (including the power button) for free with PRO mode. 🎯 New Actions • Run shell commands From e10b6537af38e37821f1ea01950d3de946dda66a Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 14:58:43 +0100 Subject: [PATCH 23/37] #1889 show tip for screen off remapping volume buttons with pro mode --- .../base/onboarding/OnboardingTipDelegate.kt | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt index 7b950dab51..33ced838b9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt @@ -177,6 +177,12 @@ class OnboardingTipDelegateImpl @Inject constructor( val hasBackKey = trigger.keys.any { it is KeyEventTriggerKey && it.keyCode == KeyEvent.KEYCODE_BACK } val hasImeKey = trigger.keys.any { it is KeyEventTriggerKey && it.requiresIme } + val hasVolumeKey = trigger.keys + .filterIsInstance() + .any { + it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + it.keyCode == KeyEvent.KEYCODE_VOLUME_UP + } when { showPowerButtonEmergencyTip -> { @@ -212,17 +218,16 @@ class OnboardingTipDelegateImpl @Inject constructor( triggerTip.value = tipModel } - // DISABLE UNTIL PRO MODE IS STABLE -// hasVolumeKey && !shownVolumeButtonsProModeTip -> { -// val tip = OnboardingTipModel( -// id = VOLUME_BUTTONS_PRO_MODE_TIP_ID, -// title = getString(R.string.tip_volume_buttons_pro_mode_title), -// message = getString(R.string.tip_volume_buttons_pro_mode_text), -// isDismissable = true, -// buttonText = getString(R.string.tip_volume_buttons_pro_mode_button), -// ) -// triggerTip.value = tip -// } + hasVolumeKey && !shownVolumeButtonsProModeTip -> { + val tip = OnboardingTipModel( + id = VOLUME_BUTTONS_PRO_MODE_TIP_ID, + title = getString(R.string.tip_volume_buttons_pro_mode_title), + message = getString(R.string.tip_volume_buttons_pro_mode_text), + isDismissable = true, + buttonText = getString(R.string.tip_volume_buttons_pro_mode_button), + ) + triggerTip.value = tip + } hasCapsLockKey && !shownCapsLockProModeTip -> { val tip = OnboardingTipModel( From 701a35ee1ffdb5aeff4fea850ff29c58fa8f982e Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 15:12:49 +0100 Subject: [PATCH 24/37] fix: check for root permission on IO thread --- .../sds100/keymapper/system/root/SuAdapter.kt | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt index 97f911ba8c..d0ce05af92 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt @@ -5,19 +5,36 @@ import io.github.sds100.keymapper.system.shell.BaseShellAdapter import io.github.sds100.keymapper.system.shell.ShellAdapter import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber @Singleton -class SuAdapterImpl @Inject constructor() : +class SuAdapterImpl @Inject constructor(private val coroutineScope: CoroutineScope) : BaseShellAdapter(), SuAdapter { - override val isRootGranted: MutableStateFlow = MutableStateFlow(getIsRooted()) + override val isRootGranted: MutableStateFlow = MutableStateFlow(false) + + private var invalidateJob: Job? = null + + init { + invalidateJob?.cancel() + invalidateJob = coroutineScope.launch { + invalidateIsRooted() + } + } override fun requestPermission() { - invalidateIsRooted() + invalidateJob?.cancel() + invalidateJob = coroutineScope.launch { + invalidateIsRooted() + } } override fun prepareCommand(command: String): Array { @@ -25,7 +42,7 @@ class SuAdapterImpl @Inject constructor() : return arrayOf("su", "-c", command) } - fun invalidateIsRooted() { + private suspend fun invalidateIsRooted() { try { // Close the shell so a new one is started without root permission. val isRooted = getIsRooted() @@ -41,10 +58,12 @@ class SuAdapterImpl @Inject constructor() : } } - private fun getIsRooted(): Boolean { - Shell.getShell().waitAndClose() - val isRooted = Shell.isAppGrantedRoot() ?: false - return isRooted + // Must execute on a separate thread so it doesn't block the Main thread. + private suspend fun getIsRooted(): Boolean { + return withContext(Dispatchers.IO) { + Shell.getShell().waitAndClose() + Shell.isAppGrantedRoot() ?: false + } } } From e2fbab43781f3181923c753081e6802346cf96cf Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 15:21:15 +0100 Subject: [PATCH 25/37] fix: do not log spam Failed to grab evdev devices --- .../github/sds100/keymapper/base/input/InputEventHub.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 4aa5ca5ce8..c557db49e1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -21,6 +21,7 @@ import io.github.sds100.keymapper.sysbridge.IEvdevCallback import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import io.github.sds100.keymapper.sysbridge.manager.isConnected +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent import io.github.sds100.keymapper.system.inputevents.KMInputEvent @@ -304,8 +305,11 @@ class InputEventHubImpl @Inject constructor( .onSuccess { result -> Timber.i("Grabbed evdev devices [${evdevDevices.joinToString { it.name }}]") } - .onFailure { - Timber.e("Failed to grab evdev devices.") + .onFailure { error -> + // Do not log if it is expected to prevent log spam. + if (error !is SystemBridgeError.Disconnected) { + Timber.e("Failed to grab evdev devices.") + } } } From 10d0317e1dc9ed0db073e8ed8c814366bbed9b25 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 15:30:44 +0100 Subject: [PATCH 26/37] improve logging system bridge connections --- .../sds100/keymapper/base/BaseKeyMapperApp.kt | 7 ++++++ .../sds100/keymapper/base/BaseMainActivity.kt | 2 +- .../manager/SystemBridgeConnectionManager.kt | 25 +++++++++++++------ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index 7cea935435..0ea1d4b5c2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -29,6 +29,7 @@ import io.github.sds100.keymapper.data.repositories.LogRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepositoryImpl import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.manager.isConnected import io.github.sds100.keymapper.system.apps.AndroidPackageManagerAdapter import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl @@ -224,6 +225,12 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { autoGrantPermissionController.start() keyEventRelayServiceWrapper.bind() + if (systemBridgeConnectionManager.isConnected()) { + Timber.i("KeyMapperApp: System bridge is connected") + } else { + Timber.i("KeyMapperApp: System bridge is disconnected") + } + if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { systemBridgeAutoStarter.init() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 9b19ba1328..7b8f927f18 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -201,7 +201,7 @@ abstract class BaseMainActivity : AppCompatActivity() { // the activities have not necessarily resumed at that point. permissionAdapter.onPermissionsChanged() serviceAdapter.invalidateState() - suAdapter.invalidateIsRooted() + suAdapter.requestPermission() systemBridgeSetupController.invalidateSettings() networkAdapter.invalidateState() onboardingUseCase.handledMigrateScreenOffKeyMapsNotification() diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 04edccdda9..9ad1cb0da2 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -92,6 +92,11 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( fun onBinderReceived(binder: IBinder) { val systemBridge = ISystemBridge.Stub.asInterface(binder) + // Can not use Timber because the content provider is called before the application's + // onCreate where the Timber Tree is installed. The content provider then + // calls this message. + Log.i(TAG, "Received system bridge binder") + synchronized(systemBridgeLock) { if (systemBridge.versionCode == buildConfigProvider.versionCode) { // Only link to death if it is the same version code so restarting it @@ -111,16 +116,20 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( time = SystemClock.elapsedRealtime(), ) } + + // Use Timber here even though it may not be planted. The Application class + // will check whether it is connected when it plants the Timber tree. + Timber.i("ConnectionManager: System bridge connected") } else { - coroutineScope.launch(Dispatchers.IO) { - // Can not use Timber because the content provider is called before the application's - // onCreate where the Timber Tree is installed. The content provider then - // calls this message. - Log.w( - TAG, - "System Bridge version mismatch! Restarting it. App: ${buildConfigProvider.versionCode}, System Bridge: ${systemBridge.versionCode}", - ) + // Can not use Timber because the content provider is called before the application's + // onCreate where the Timber Tree is installed. The content provider then + // calls this message. + Log.w( + TAG, + "System Bridge version mismatch! Restarting it. App: ${buildConfigProvider.versionCode}, System Bridge: ${systemBridge.versionCode}", + ) + coroutineScope.launch(Dispatchers.IO) { restartSystemBridge(systemBridge) } } From 628b31e4d53afdc450d06079bdfe3fb373090282 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 15:30:53 +0100 Subject: [PATCH 27/37] update strings --- base/src/main/res/values/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index c21c3a5203..52dfe8fb0b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -82,7 +82,7 @@ Trigger device not connected! Migrate this screen off trigger - Your key maps will stop working randomly! + Disable battery optimisation. Your key maps are paused! Unpause The accessibility service needs to be turned on for your key maps to work! @@ -448,7 +448,7 @@ Key Mapper was interrupted - Key Mapper tried to run in the background but was stopped by the system.\nThis can happen if you have battery or memory optimization turned on.\n\nTo fix this, you can try following an online guide. You should also restart the service when you\'re done. + Key Mapper tried to run in the background but was stopped by the system.\nThis can happen if you have battery or memory optimisation turned on.\n\nTo fix this, you can try following an online guide. You should also restart the service when you\'re done. Proceed Ignore @@ -1368,7 +1368,7 @@ PRO mode provides better compatibility for Caps Lock remapping. Tap \'Use PRO mode\' and record it again. Use PRO mode Screen off remapping? - Use PRO mode if you want your trigger to work when the screen is off. Tap \'Use PRO mode\' and record it again. + Any trigger can now work when the screen is off for free with PRO mode! You will need to record it again. Use PRO mode App pinning warning Using the back button as a trigger may conflict with app pinning, causing a black screen on unlock. A reboot will fix it. From ea645242754c3d969e04604ac8a2a1e5236a0535 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 15:43:55 +0100 Subject: [PATCH 28/37] fix: create notification channel for system bridge setup in NotificationController There was a system error when showing the notification from SystemBridgeAutoStarter because the channel was only created in SystemBridgeSetupAssistantController. --- .../promode/SystemBridgeSetupAssistantController.kt | 13 ------------- .../system/notifications/NotificationController.kt | 8 ++++++++ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index d7e0ae42db..8bdbfb44d4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -5,7 +5,6 @@ import android.os.Build import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import androidx.annotation.RequiresApi -import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -29,7 +28,6 @@ import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManage import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep -import io.github.sds100.keymapper.system.notifications.NotificationChannelModel import io.github.sds100.keymapper.system.notifications.NotificationModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -110,8 +108,6 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private var foundPairingCode: String? = null fun onServiceConnected() { - createNotificationChannel() - coroutineScope.launch { setupController.setupAssistantStep.collect { step -> if (step == null) { @@ -140,15 +136,6 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } } - private fun createNotificationChannel() { - val notificationChannel = NotificationChannelModel( - id = NotificationController.Companion.CHANNEL_SETUP_ASSISTANT, - name = getString(R.string.pro_mode_setup_assistant_notification_channel), - importance = NotificationManagerCompat.IMPORTANCE_MAX, - ) - manageNotifications.createChannel(notificationChannel) - } - fun teardown() { dismissNotification() stopInteracting() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index 37ffa5125a..7bdb118f7d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -102,6 +102,14 @@ class NotificationController @Inject constructor( ), ) + manageNotifications.createChannel( + NotificationChannelModel( + id = CHANNEL_SETUP_ASSISTANT, + name = getString(R.string.pro_mode_setup_assistant_notification_channel), + importance = NotificationManagerCompat.IMPORTANCE_MAX, + ), + ) + combine( controlAccessibilityService.serviceState, pauseMappings.isPaused, From 27129fbe374ce080d78dfc1241c1a5f975472366 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 23:12:21 +0100 Subject: [PATCH 29/37] add more logging to system bridge starting --- .../keymapper/base/promode/SystemBridgeAutoStarter.kt | 6 ++++++ .../base/promode/SystemBridgeSetupAssistantController.kt | 7 ++++--- .../sysbridge/service/SystemBridgeSetupController.kt | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt index 9c949825bf..4c9e5fe176 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt @@ -17,6 +17,7 @@ import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.BuildConfig import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.manager.isConnected import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.network.NetworkAdapter import io.github.sds100.keymapper.system.notifications.NotificationAdapter @@ -225,6 +226,11 @@ class SystemBridgeAutoStarter @Inject constructor( return } + if (connectionManager.isConnected()) { + Timber.i("Not auto starting with $type because already connected.") + return + } + lastAutoStartTime = SystemClock.elapsedRealtime() when (type) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt index 8bdbfb44d4..464524777f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -219,8 +219,6 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private suspend fun onPairingSuccess() { setupController.startWithAdb() - stopInteracting() - val isStarted = try { withTimeout(10000L) { systemBridgeConnectionManager.connectionState @@ -234,6 +232,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } if (isStarted) { + Timber.i("System bridge started after pairing. Going back to Key Mapper.") getKeyMapperAppTask()?.moveToFront() } else { Timber.e("Failed to start system bridge after pairing.") @@ -245,6 +244,8 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( ), ) } + + stopInteracting() } private fun clickPairWithCodeButton(rootNode: AccessibilityNodeInfo) { @@ -369,7 +370,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( private fun getKeyMapperAppTask(): ActivityManager.AppTask? { val task = activityManager.appTasks - .firstOrNull { + ?.firstOrNull { it.taskInfo.topActivity?.className == keyMapperClassProvider.getMainActivity().name } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 8d30f0a6c4..beedaf047c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -318,7 +318,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private fun getKeyMapperAppTask(): ActivityManager.AppTask? { val task = activityManager.appTasks - .firstOrNull { + ?.firstOrNull { it.taskInfo.topActivity?.className == keyMapperClassProvider.getMainActivity().name } From af175117dd3e52f33bdb74e97dca3c2df4d71973 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 23:28:47 +0100 Subject: [PATCH 30/37] fix: Starting system bridge for the first time would be janky because granting READ_LOGS kills the app process. Only grant for READ_LOGS when sharing logcat from settings. --- CHANGELOG.md | 3 +- .../base/logging/ShareLogcatUseCase.kt | 14 +++++++++ .../base/settings/SettingsViewModel.kt | 30 +++++++++++++++---- .../permissions/RequestPermissionDelegate.kt | 2 ++ .../sds100/keymapper/base/utils/ErrorUtils.kt | 2 ++ base/src/main/res/values/strings.xml | 6 ++++ .../manager/SystemBridgeConnectionManager.kt | 6 ++-- .../permissions/AndroidPermissionAdapter.kt | 7 +++++ .../system/permissions/Permission.kt | 1 + 9 files changed, 63 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 274dea5cb0..5f7987b9a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ## Fixed -- Only autostart PRO mode with Shizuku if Shizuku permission is granted. Otherwise fallback to method with Wireless Debugging and WRITE_SECURE_SETTINGS permission +- Only autostart PRO mode with Shizuku if Shizuku permission is granted. Otherwise fallback to method with Wireless Debugging and WRITE_SECURE_SETTINGS permission. +- Starting system bridge for the first time would be janky because granting READ_LOGS kills the app process. Only grant for READ_LOGS when sharing logcat from settings. ## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/ShareLogcatUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/ShareLogcatUseCase.kt index 4565769d8b..951cde08aa 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/ShareLogcatUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/ShareLogcatUseCase.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.logging +import android.Manifest import android.content.Context import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext @@ -12,6 +13,8 @@ import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.system.files.FileAdapter import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.files.IFile +import io.github.sds100.keymapper.system.permissions.Permission +import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.shell.ShellAdapter import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -22,9 +25,18 @@ class ShareLogcatUseCaseImpl @Inject constructor( @ApplicationContext private val ctx: Context, private val fileAdapter: FileAdapter, private val shellAdapter: ShellAdapter, + private val permissionAdapter: PermissionAdapter, private val buildConfigProvider: BuildConfigProvider, ) : ShareLogcatUseCase { + override fun isPermissionGranted(): Boolean { + return permissionAdapter.isGranted(Permission.READ_LOGS) + } + + override fun grantPermission(): KMResult<*> { + return permissionAdapter.grant(Manifest.permission.READ_LOGS) + } + override suspend fun share(): KMResult { val fileName = "logs/logcat_${FileUtils.createFileDate()}.txt" @@ -45,5 +57,7 @@ class ShareLogcatUseCaseImpl @Inject constructor( } interface ShareLogcatUseCase { + fun isPermissionGranted(): Boolean + fun grantPermission(): KMResult<*> suspend fun share(): KMResult } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 201fe308be..540bf753fa 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -293,12 +293,32 @@ class SettingsViewModel @Inject constructor( fun onShareLogcatClick() { viewModelScope.launch { - shareLogcatUseCase.share().onFailure { error -> - val dialog = DialogModel.Ok( - title = getString(R.string.dialog_title_share_logcat_error), - message = error.getFullMessage(this@SettingsViewModel), + if (shareLogcatUseCase.isPermissionGranted()) { + shareLogcatUseCase.share().onFailure { error -> + val dialog = DialogModel.Ok( + title = getString(R.string.dialog_title_share_logcat_error), + message = error.getFullMessage(this@SettingsViewModel), + ) + showDialog("logcat_error", dialog) + } + } else { + val dialog = DialogModel.Alert( + title = getString(R.string.dialog_title_grant_read_logs_permission_title), + message = getString(R.string.dialog_title_grant_read_logs_permission_message), + positiveButtonText = getString(R.string.pos_proceed), + negativeButtonText = getString(R.string.neg_cancel), ) - showDialog("logcat_error", dialog) + + val response = showDialog("read_logs_permission", dialog) + + if (response == DialogResponse.POSITIVE) { + shareLogcatUseCase.grantPermission().onFailure { error -> + val dialog = DialogModel.Ok( + message = error.getFullMessage(resourceProvider), + ) + showDialog("grant_read_log_failure", dialog) + } + } } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt index 7db51d9faa..22067867e6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt @@ -110,6 +110,8 @@ class RequestPermissionDelegate( requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } } + + Permission.READ_LOGS -> permissionAdapter.grant(Manifest.permission.READ_LOGS) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt index 5a835bc1e2..a9214a7152 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt @@ -57,6 +57,8 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { R.string.error_find_nearby_devices_permission_denied Permission.POST_NOTIFICATIONS -> R.string.error_notifications_permission_denied + Permission.READ_LOGS -> + R.string.error_read_logs_permission_denied } resourceProvider.getString(resId) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 52dfe8fb0b..17f2e06498 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -481,6 +481,10 @@ We\'re sorry for the disruption. We\'ve introduced a *free* feature called PRO mode that replaces the old screen off remapping option. This feature is much more reliable and unlocks remapping the power button too.\n\nDue to the way it works you will need to record this trigger again with PRO mode. Proceed + Grant READ_LOGS permission + You will need to be rooted or have enabled PRO mode for this to work. The system will then close the app when it grants this permission. You will need to open it again yourself and try sharing the logcat again. + + Done Kill Guide @@ -495,6 +499,7 @@ Save Understood Turn on + Proceed Turn off Cancel @@ -868,6 +873,7 @@ Denied permission to answer and end phone calls! Denied permission to see paired Bluetooth devices! Denied permission to show notifications! + Denied permission to read logs! Must be 2 or more! Must be %d or less! Must be greater than 0! diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 9ad1cb0da2..58db6df87f 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -205,9 +205,11 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( -1 } + // WARNING! Granting some permissions (e.g READ_LOGS) will cause the system to kill + // the app process and restart it. This is normal, expected behavior and can not be + // worked around. Do not grant any other permissions automatically here. systemBridge.grantPermission(Manifest.permission.WRITE_SECURE_SETTINGS, deviceId) - systemBridge.grantPermission(Manifest.permission.READ_LOGS, deviceId) - Timber.i("Granted WRITE_SECURE_SETTINGS and READ_LOGS permission with System Bridge") + Timber.i("Granted WRITE_SECURE_SETTINGS permission with System Bridge") if (ContextCompat.checkSelfPermission( ctx, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt index 6054aa3c9c..6f33664c9e 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt @@ -304,6 +304,13 @@ class AndroidPermissionAdapter @Inject constructor( } else { true } + + Permission.READ_LOGS -> { + ContextCompat.checkSelfPermission( + ctx, + Manifest.permission.READ_LOGS, + ) == PERMISSION_GRANTED + } } override fun isGrantedFlow(permission: Permission): Flow = channelFlow { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt index 468b91aba3..f243a54b0c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt @@ -17,4 +17,5 @@ enum class Permission { ANSWER_PHONE_CALL, FIND_NEARBY_DEVICES, POST_NOTIFICATIONS, + READ_LOGS, } From 19900b44415dd8235f7b7301dddceafdb52eb4f3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 23:30:14 +0100 Subject: [PATCH 31/37] style: add ktlint to systemstubs module --- systemstubs/build.gradle.kts | 1 + .../src/main/java/android/app/ActivityTaskManagerApis.kt | 6 +++--- systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt | 2 +- .../main/java/android/permission/PermissionManagerApis.kt | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/systemstubs/build.gradle.kts b/systemstubs/build.gradle.kts index 9c067452a1..07a1908d86 100644 --- a/systemstubs/build.gradle.kts +++ b/systemstubs/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.jlleitschuh.gradle.ktlint) } android { diff --git a/systemstubs/src/main/java/android/app/ActivityTaskManagerApis.kt b/systemstubs/src/main/java/android/app/ActivityTaskManagerApis.kt index a980d7dcc4..ac3a80e5f6 100644 --- a/systemstubs/src/main/java/android/app/ActivityTaskManagerApis.kt +++ b/systemstubs/src/main/java/android/app/ActivityTaskManagerApis.kt @@ -9,14 +9,14 @@ object ActivityTaskManagerApis { maxNum: Int, filterOnlyVisibleRecents: Boolean, keepIntentExtra: Boolean, - displayId: Int + displayId: Int, ): MutableList? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return activityTaskManager.getTasks( maxNum, filterOnlyVisibleRecents, keepIntentExtra, - displayId + displayId, ) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return activityTaskManager.getTasks(maxNum, filterOnlyVisibleRecents, keepIntentExtra) @@ -24,4 +24,4 @@ object ActivityTaskManagerApis { return activityTaskManager.getTasks(maxNum) } } -} \ No newline at end of file +} diff --git a/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt b/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt index 3f7dce07c9..d3f52096ea 100644 --- a/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt +++ b/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt @@ -18,4 +18,4 @@ object NfcAdapterApis { adapter.disable(saveState) } } -} \ No newline at end of file +} diff --git a/systemstubs/src/main/java/android/permission/PermissionManagerApis.kt b/systemstubs/src/main/java/android/permission/PermissionManagerApis.kt index c5983b5a34..8ef810c186 100644 --- a/systemstubs/src/main/java/android/permission/PermissionManagerApis.kt +++ b/systemstubs/src/main/java/android/permission/PermissionManagerApis.kt @@ -8,7 +8,7 @@ object PermissionManagerApis { packageName: String, permission: String, deviceId: Int, - userId: Int + userId: Int, ) { // In revisions of Android 14 the method to grant permissions changed // so try them all. @@ -51,4 +51,4 @@ object PermissionManagerApis { ) } } -} \ No newline at end of file +} From eb4b3855c545f645f73e413fc3823b7d3b1b47a6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 7 Nov 2025 23:58:52 +0100 Subject: [PATCH 32/37] #1886 fix: mobile data actions work in obfuscated builds --- CHANGELOG.md | 1 + app/proguard-rules.pro | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7987b9a0..6edf33766b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Only autostart PRO mode with Shizuku if Shizuku permission is granted. Otherwise fallback to method with Wireless Debugging and WRITE_SECURE_SETTINGS permission. - Starting system bridge for the first time would be janky because granting READ_LOGS kills the app process. Only grant for READ_LOGS when sharing logcat from settings. +- #1886 mobile data actions work in PRO mode ## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 032e987d4b..8127b3ee91 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -75,6 +75,8 @@ -keep class io.github.sds100.keymapper.api.IKeyEventRelayService$Stub { *; } -keep class io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback { *; } -keep class io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback$Stub { *; } +-keep class com.android.internal.telephony.ITelephony { *; } +-keep class com.android.internal.telephony.ITelephony$Stub { *; } -keepattributes *Annotation*, InnerClasses -dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations From a1e5744538a222551649e815663d295e9ff60eb0 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 8 Nov 2025 00:00:12 +0100 Subject: [PATCH 33/37] #1890 fix: copy log to clipboard in reverse order so the most recent entries are not cut off --- .../base/logging/DisplayLogUseCase.kt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt index fee1623fc8..0a4c9ff010 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt @@ -40,7 +40,7 @@ class DisplayLogUseCaseImpl @Inject constructor( override suspend fun copyToClipboard() { val logEntries = repository.log.first() - val logText = createLogText(logEntries) + val logText = createLogClipboardText(logEntries) clipboardAdapter.copy( label = resourceProvider.getString(R.string.clip_key_mapper_log), @@ -48,11 +48,20 @@ class DisplayLogUseCaseImpl @Inject constructor( ) } - private fun createLogText(logEntries: List): String { - return logEntries.joinToString(separator = "\n") { entry -> - val date = dateFormat.format(Date(entry.time)) + private fun createLogClipboardText(logEntries: List): String { + return buildString { + append("Key Mapper log (newest first). Note: it may be cut off due to clipboard limits") + appendLine() + appendLine() - return@joinToString "$date ${severityString[entry.severity]} ${entry.message}" + logEntries + .reversed() + .joinToString(separator = "\n") { entry -> + val date = dateFormat.format(Date(entry.time)) + + return@joinToString "$date ${severityString[entry.severity]} ${entry.message}" + } + .also { append(it) } } } } From 9e25597bc0544577129c7eb2a5551b594faa736a Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 8 Nov 2025 00:08:03 +0100 Subject: [PATCH 34/37] delete unused menu xml files --- base/src/main/res/menu/menu_log.xml | 28 ------------------------ base/src/main/res/menu/menu_settings.xml | 10 --------- 2 files changed, 38 deletions(-) delete mode 100644 base/src/main/res/menu/menu_log.xml delete mode 100644 base/src/main/res/menu/menu_settings.xml diff --git a/base/src/main/res/menu/menu_log.xml b/base/src/main/res/menu/menu_log.xml deleted file mode 100644 index d23e3dd5ac..0000000000 --- a/base/src/main/res/menu/menu_log.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/base/src/main/res/menu/menu_settings.xml b/base/src/main/res/menu/menu_settings.xml deleted file mode 100644 index 9ac5830634..0000000000 --- a/base/src/main/res/menu/menu_settings.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file From 8394585958018448459e014c248ecb23d905d6db Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 8 Nov 2025 00:12:24 +0100 Subject: [PATCH 35/37] #1890 feat: add button to save log to file and share it --- CHANGELOG.md | 6 ++- .../base/logging/DisplayLogUseCase.kt | 47 +++++++++++++++++-- .../keymapper/base/logging/LogScreen.kt | 10 ++++ .../keymapper/base/logging/LogViewModel.kt | 6 +++ base/src/main/res/values/strings.xml | 2 +- 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6edf33766b..a19aa3dcb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,15 @@ #### TO BE RELEASED +## Added + +- #1890 add button to save log to file and share it. The clipboard button now cuts off older entries and keeps newest ones. + ## Fixed - Only autostart PRO mode with Shizuku if Shizuku permission is granted. Otherwise fallback to method with Wireless Debugging and WRITE_SECURE_SETTINGS permission. - Starting system bridge for the first time would be janky because granting READ_LOGS kills the app process. Only grant for READ_LOGS when sharing logcat from settings. -- #1886 mobile data actions work in PRO mode +- #1886 mobile data actions work in PRO mode. ## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt index 0a4c9ff010..afb92c66c0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt @@ -1,26 +1,39 @@ package io.github.sds100.keymapper.base.logging +import android.content.Context +import androidx.core.net.toUri +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.utils.ShareUtils import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.data.repositories.LogRepository import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter import io.github.sds100.keymapper.system.files.FileAdapter +import io.github.sds100.keymapper.system.files.FileUtils +import io.github.sds100.keymapper.system.files.IFile import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class DisplayLogUseCaseImpl @Inject constructor( + @ApplicationContext private val ctx: Context, + private val coroutineScope: CoroutineScope, private val repository: LogRepository, private val resourceProvider: ResourceProvider, private val clipboardAdapter: ClipboardAdapter, private val fileAdapter: FileAdapter, + private val buildConfigProvider: BuildConfigProvider, ) : DisplayLogUseCase { private val dateFormat = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault()) private val severityString: Map = mapOf( @@ -56,12 +69,37 @@ class DisplayLogUseCaseImpl @Inject constructor( logEntries .reversed() - .joinToString(separator = "\n") { entry -> - val date = dateFormat.format(Date(entry.time)) + .joinToString(separator = "\n", transform = ::entryToString) + .also { append(it) } + } + } + + private fun entryToString(entry: LogEntryEntity): String { + val date = dateFormat.format(Date(entry.time)) + + return "$date ${severityString[entry.severity]} ${entry.message}" + } - return@joinToString "$date ${severityString[entry.severity]} ${entry.message}" + override fun shareFile() { + val fileName = "logs/key_mapper_log_${FileUtils.createFileDate()}.txt" + + coroutineScope.launch { + withContext(Dispatchers.IO) { + val logEntries = repository.log.first() + val logText = logEntries.joinToString(separator = "\n", transform = ::entryToString) + + val file: IFile = fileAdapter.getPrivateFile(fileName) + file.createFile() + + with(file.outputStream()?.bufferedWriter()) { + this?.write(logText) + this?.flush() } - .also { append(it) } + + val publicUri = fileAdapter.getPublicUriForPrivateFile(file) + + ShareUtils.shareFile(ctx, publicUri.toUri(), buildConfigProvider.packageName) + } } } } @@ -70,4 +108,5 @@ interface DisplayLogUseCase { val log: Flow> fun clearLog() suspend fun copyToClipboard() + fun shareFile() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt index 13e7fbf58e..e57d3d728f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -53,6 +54,7 @@ fun LogScreen( modifier = modifier, onBackClick = onBackClick, onCopyToClipboardClick = viewModel::onCopyToClipboardClick, + onShareClick = viewModel::onShareFileClick, onClearLogClick = viewModel::onClearLogClick, content = { Content( @@ -69,6 +71,7 @@ private fun LogScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit = {}, onCopyToClipboardClick: () -> Unit = {}, + onShareClick: () -> Unit = {}, onClearLogClick: () -> Unit = {}, content: @Composable () -> Unit, ) { @@ -96,6 +99,13 @@ private fun LogScreen( ) } Spacer(Modifier.weight(1f)) + IconButton(onClick = onShareClick) { + Icon( + imageVector = Icons.Outlined.Share, + contentDescription = stringResource(R.string.action_share_log), + ) + } + IconButton(onClick = onCopyToClipboardClick) { Icon( imageVector = Icons.Outlined.ContentCopy, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt index b40264e249..5e6156ca63 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt @@ -42,6 +42,12 @@ class LogViewModel @Inject constructor(private val displayLogUseCase: DisplayLog } } + fun onShareFileClick() { + viewModelScope.launch { + displayLogUseCase.shareFile() + } + } + fun onClearLogClick() { displayLogUseCase.clearLog() } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 17f2e06498..2a2fadb1d3 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -388,7 +388,7 @@ Back up everything Tap to pause Tap to resume - Save + Share Toggle short messages Copy Clear From 288bae3f09deec1c7203b69045528c263f87dc0d Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 8 Nov 2025 00:15:23 +0100 Subject: [PATCH 36/37] chore: bump version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index 5e1ed47487..886f7bcec6 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ VERSION_NAME=4.0.0-beta.2 -VERSION_CODE=186 +VERSION_CODE=187 VERSION_NUM=01 \ No newline at end of file From a79c1962a46746748f5324d2301583b5455f78fb Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 8 Nov 2025 00:15:55 +0100 Subject: [PATCH 37/37] chore: set release date in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a19aa3dcb8..38c136de1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [4.0.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.02) -#### TO BE RELEASED +#### 08 November 2025 ## Added