From f5a5ac3714e72a744d5b2a56b1b4020cdd95d438 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 4 Jun 2026 11:20:35 +0200 Subject: [PATCH 01/12] update whats-new --- base/src/main/assets/whats-new.txt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/base/src/main/assets/whats-new.txt b/base/src/main/assets/whats-new.txt index fab11d04bb..35cd3d1f91 100644 --- a/base/src/main/assets/whats-new.txt +++ b/base/src/main/assets/whats-new.txt @@ -1,9 +1,6 @@ -🎯 New Actions -• Select all text at the cursor -• Input on-screen keyboard enter/send button -• Show a toast message - -🔧 Improvements -• Bug fixes +• Talkback actions +• Monochrome app icon +• "Any input device" is the default for triggers +• Bug fixes and accessibility improvements 📖 View the complete changelog at: http://keymapper.app/changelog From ef0ae7833e52f2040af1276b14360725fc627d6d Mon Sep 17 00:00:00 2001 From: smh786 Date: Fri, 5 Jun 2026 20:43:40 +0100 Subject: [PATCH 02/12] Delay accessibility service init until user unlock --- .../accessibility/MyAccessibilityService.kt | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt index bc47591aae..1fdae9a795 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt @@ -1,6 +1,10 @@ package io.github.sds100.keymapper.system.accessibility import android.content.Intent +import android.os.UserManager +import android.view.KeyEvent +import android.view.accessibility.AccessibilityEvent +import androidx.core.content.getSystemService import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityService import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController @@ -14,6 +18,9 @@ class MyAccessibilityService : BaseAccessibilityService() { lateinit var controllerFactory: AccessibilityServiceController.Factory private var controller: AccessibilityServiceController? = null + private var loggedLockedInitDelay = false + + private val userManager: UserManager? by lazy { getSystemService() } override fun getController(): BaseAccessibilityServiceController? { return controller @@ -22,15 +29,47 @@ class MyAccessibilityService : BaseAccessibilityService() { override fun onServiceConnected() { super.onServiceConnected() + initializeControllerIfUserUnlocked() + } + + override fun onAccessibilityEvent(event: AccessibilityEvent?) { + if (!initializeControllerIfUserUnlocked()) { + return + } + + super.onAccessibilityEvent(event) + } + + override fun onKeyEvent(event: KeyEvent?): Boolean { + if (!initializeControllerIfUserUnlocked()) { + return false + } + + return super.onKeyEvent(event) + } + + private fun initializeControllerIfUserUnlocked(): Boolean { + if (userManager?.isUserUnlocked == false) { + if (!loggedLockedInitDelay) { + Timber.i("Accessibility service: Delay init because locked.") + loggedLockedInitDelay = true + } + + return false + } + + loggedLockedInitDelay = false + /* I would put this in onCreate but for some reason on some devices getting the application context would return null */ if (controller == null) { controller = controllerFactory.create(this) + controller?.onServiceConnected() } - controller?.onServiceConnected() + return true } override fun onUnbind(intent: Intent?): Boolean { From 47c620a3c4c5800da6075ac61c9ab9f0d6f7d081 Mon Sep 17 00:00:00 2001 From: smh786 Date: Fri, 5 Jun 2026 22:17:45 +0100 Subject: [PATCH 03/12] Fix Direct Boot accessibility startup --- app/src/main/AndroidManifest.xml | 4 +- .../sds100/keymapper/base/BaseKeyMapperApp.kt | 93 ++++++++++--------- .../keymapper/base/BootBroadcastReceiver.kt | 5 +- .../accessibility/BaseAccessibilityService.kt | 11 ++- 4 files changed, 63 insertions(+), 50 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d74d55e0eb..424e398db2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,6 @@ @@ -67,4 +67,4 @@ - \ No newline at end of file + 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 ee9e54e816..4bf021c4fd 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 @@ -36,6 +36,7 @@ import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.permissions.Permission import java.util.Calendar import javax.inject.Inject +import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first @@ -51,46 +52,46 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { private val tag = BaseKeyMapperApp::class.simpleName @Inject - lateinit var appCoroutineScope: CoroutineScope + lateinit var appCoroutineScope: Lazy @Inject - lateinit var notificationController: NotificationController + lateinit var notificationController: Lazy @Inject - lateinit var packageManagerAdapter: AndroidPackageManagerAdapter + lateinit var packageManagerAdapter: Lazy @Inject - lateinit var devicesAdapter: AndroidDevicesAdapter + lateinit var devicesAdapter: Lazy @Inject - lateinit var permissionAdapter: AndroidPermissionAdapter + lateinit var permissionAdapter: Lazy @Inject - lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl + lateinit var accessibilityServiceAdapter: Lazy @Inject - lateinit var autoGrantPermissionController: AutoGrantPermissionController + lateinit var autoGrantPermissionController: Lazy @Inject - lateinit var loggingTree: KeyMapperLoggingTree + lateinit var loggingTree: Lazy @Inject - lateinit var settingsRepository: PreferenceRepositoryImpl + lateinit var settingsRepository: Lazy @Inject - lateinit var logRepository: LogRepository + lateinit var logRepository: Lazy @Inject - lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl + lateinit var keyEventRelayServiceWrapper: Lazy @Inject - lateinit var systemBridgeAutoStarter: SystemBridgeAutoStarter + lateinit var systemBridgeAutoStarter: Lazy @Inject - lateinit var systemBridgeConnectionManager: SystemBridgeConnectionManagerImpl + lateinit var systemBridgeConnectionManager: Lazy @Inject - lateinit var systemBridgeLogger: SystemBridgeLogger + lateinit var systemBridgeLogger: Lazy private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() } @@ -118,16 +119,18 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { Log.i(tag, "KeyMapperApp: OnCreate") Thread.setDefaultUncaughtExceptionHandler { thread, exception -> - // log in a blocking manner and always log regardless of whether the setting is turned on - val entry = LogEntryEntity( - id = 0, - time = Calendar.getInstance().timeInMillis, - severity = LogEntryEntity.SEVERITY_ERROR, - message = exception.stackTraceToString(), - ) - - runBlocking { - logRepository.insertSuspend(entry) + if (userManager?.isUserUnlocked != false) { + // log in a blocking manner and always log regardless of whether the setting is turned on + val entry = LogEntryEntity( + id = 0, + time = Calendar.getInstance().timeInMillis, + severity = LogEntryEntity.SEVERITY_ERROR, + message = exception.stackTraceToString(), + ) + + runBlocking { + logRepository.get().insertSuspend(entry) + } } priorExceptionHandler?.uncaughtException(thread, exception) @@ -168,7 +171,7 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { registerReceiver(broadcastReceiver, intentFilter) - settingsRepository.get(Keys.darkTheme) + settingsRepository.get().get(Keys.darkTheme) .map { it?.toIntOrNull() } .map { when (it) { @@ -178,15 +181,15 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { } } .onEach { mode -> AppCompatDelegate.setDefaultNightMode(mode) } - .launchIn(appCoroutineScope) + .launchIn(appCoroutineScope.get()) if (BuildConfig.BUILD_TYPE == "debug" || BuildConfig.BUILD_TYPE == "debug_release") { Timber.plant(Timber.DebugTree()) } - Timber.plant(loggingTree) + Timber.plant(loggingTree.get()) - notificationController.init() + notificationController.get().init() processLifecycleOwner.lifecycle.addObserver( object : LifecycleObserver { @@ -194,19 +197,19 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { // when the user returns to the app let everything know that the permissions could have changed - notificationController.onOpenApp() + notificationController.get().onOpenApp() if (BuildConfig.DEBUG && - permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS) + permissionAdapter.get().isGranted(Permission.WRITE_SECURE_SETTINGS) ) { - accessibilityServiceAdapter.start() + accessibilityServiceAdapter.get().start() } } }, ) - appCoroutineScope.launch { - notificationController.openApp.collectLatest { intentAction -> + appCoroutineScope.get().launch { + notificationController.get().openApp.collectLatest { intentAction -> Intent(this@BaseKeyMapperApp, getMainActivityClass()).apply { action = intentAction flags = Intent.FLAG_ACTIVITY_NEW_TASK @@ -216,38 +219,38 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { } } - notificationController.showToast.onEach { toast -> + notificationController.get().showToast.onEach { toast -> Toast.makeText(this, toast, Toast.LENGTH_SHORT).show() - }.launchIn(appCoroutineScope) + }.launchIn(appCoroutineScope.get()) - autoGrantPermissionController.start() - keyEventRelayServiceWrapper.bind() + autoGrantPermissionController.get().start() + keyEventRelayServiceWrapper.get().bind() - if (systemBridgeConnectionManager.isConnected()) { + if (systemBridgeConnectionManager.get().isConnected()) { Timber.i("KeyMapperApp: System bridge is connected") } else { Timber.i("KeyMapperApp: System bridge is disconnected") } - systemBridgeAutoStarter.init() + systemBridgeAutoStarter.get().init() // Initialize SystemBridgeLogger to start receiving log messages from SystemBridge. // Using Lazy<> to avoid circular dependency issues and ensure it's only created // when the API level requirement is met. - systemBridgeLogger.start() + systemBridgeLogger.get().start() - appCoroutineScope.launch { - systemBridgeConnectionManager.connectionState.collect { state -> + appCoroutineScope.get().launch { + systemBridgeConnectionManager.get().connectionState.collect { state -> if (state is SystemBridgeConnectionState.Connected) { val isUsed = - settingsRepository.get(Keys.isSystemBridgeUsed).first() ?: false + settingsRepository.get().get(Keys.isSystemBridgeUsed).first() ?: false // Enable the setting to use PRO mode for key event actions the first time they use PRO mode. if (!isUsed) { - settingsRepository.set(Keys.keyEventActionsUseSystemBridge, true) + settingsRepository.get().set(Keys.keyEventActionsUseSystemBridge, true) } - settingsRepository.set(Keys.isSystemBridgeUsed, true) + settingsRepository.get().set(Keys.isSystemBridgeUsed, true) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt b/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt index 7bb321b372..3d45969541 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt @@ -15,10 +15,13 @@ class BootBroadcastReceiver : BroadcastReceiver() { Timber.i( "Boot completed broadcast: time since boot = ${SystemClock.elapsedRealtime() / 1000}", ) + (context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked() } Intent.ACTION_LOCKED_BOOT_COMPLETED -> { - (context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked() + Timber.i( + "Locked boot completed broadcast: time since boot = ${SystemClock.elapsedRealtime() / 1000}", + ) } } } 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 d247d41075..ba78fa0632 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 @@ -28,6 +28,7 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint +import dagger.Lazy import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType import io.github.sds100.keymapper.base.actions.talkback.TalkbackGesturePerformer @@ -54,10 +55,16 @@ abstract class BaseAccessibilityService : SavedStateRegistryOwner { @Inject - lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl + lateinit var accessibilityServiceAdapterLazy: Lazy + + val accessibilityServiceAdapter: AccessibilityServiceAdapterImpl + get() = accessibilityServiceAdapterLazy.get() @Inject - lateinit var inputMethodAdapter: InputMethodAdapter + lateinit var inputMethodAdapterLazy: Lazy + + val inputMethodAdapter: InputMethodAdapter + get() = inputMethodAdapterLazy.get() private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) private var savedStateRegistryController: SavedStateRegistryController? = From af57c6eceec914cd0bf9df5df1efffb59fe81f2a Mon Sep 17 00:00:00 2001 From: smh786 Date: Sat, 6 Jun 2026 20:03:33 +0100 Subject: [PATCH 04/12] #2153 fix: add Direct Boot changelog entry --- CHANGELOG.md | 1 + .../java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt | 2 +- .../base/system/accessibility/BaseAccessibilityService.kt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ca051484..cbac66a102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## Fixed +- #2153 Prevent Direct Boot startup from initializing credential-encrypted app storage before the user unlocks. - #2074 Scrolling the action or trigger list no longer accidentally moves items; reordering by drag now only activates from the drag handle or via long-press. ## Changed 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 4bf021c4fd..50c1a77345 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 @@ -15,6 +15,7 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDexApplication +import dagger.Lazy import io.github.sds100.keymapper.base.expertmode.SystemBridgeAutoStarter import io.github.sds100.keymapper.base.logging.KeyMapperLoggingTree import io.github.sds100.keymapper.base.logging.SystemBridgeLogger @@ -36,7 +37,6 @@ import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.permissions.Permission import java.util.Calendar import javax.inject.Inject -import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first 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 ba78fa0632..de12b4600d 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 @@ -27,8 +27,8 @@ import androidx.lifecycle.LifecycleRegistry import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner -import dagger.hilt.android.AndroidEntryPoint import dagger.Lazy +import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType import io.github.sds100.keymapper.base.actions.talkback.TalkbackGesturePerformer From 0f66372652363aaa5de949b2242719dff5a97c34 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 00:11:58 +0000 Subject: [PATCH 05/12] #2154 fix: gate expert mode debug screen on warning acknowledgement Move the debugging section (GetEvent button) inside the `warningState is Understood` conditional so it is only visible after the user acknowledges the expert mode warning, preventing the bypass via the debug screen. Also add a defensive guard in ExpertModeViewModel.onGetEventClick() that returns early if the warning has not been acknowledged. --- CHANGELOG.md | 1 + .../base/expertmode/ExpertModeScreen.kt | 42 +++++++++---------- .../base/expertmode/ExpertModeViewModel.kt | 1 + 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ca051484..fd65797b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## Fixed +- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged. - #2074 Scrolling the action or trigger list no longer accidentally moves items; reordering by drag now only activates from the drag handle or via long-press. ## Changed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt index eb1e4c1e8b..d843f45bc1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt @@ -252,6 +252,27 @@ private fun Content( ) } } + + Spacer(modifier = Modifier.height(16.dp)) + + OptionsHeaderRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + icon = Icons.Outlined.BugReport, + text = stringResource(R.string.settings_section_debugging_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + modifier = Modifier.padding(horizontal = 8.dp), + title = stringResource(R.string.title_pref_get_event_debug), + text = stringResource(R.string.summary_pref_get_event_debug), + icon = Icons.Outlined.BugReport, + onClick = onGetEventClick, + ) + Spacer(modifier = Modifier.height(8.dp)) } else { Text( modifier = Modifier.padding(horizontal = 32.dp), @@ -259,27 +280,6 @@ private fun Content( textAlign = TextAlign.Center, ) } - - Spacer(modifier = Modifier.height(16.dp)) - - OptionsHeaderRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - icon = Icons.Outlined.BugReport, - text = stringResource(R.string.settings_section_debugging_title), - ) - - Spacer(modifier = Modifier.height(8.dp)) - - OptionPageButton( - modifier = Modifier.padding(horizontal = 8.dp), - title = stringResource(R.string.title_pref_get_event_debug), - text = stringResource(R.string.summary_pref_get_event_debug), - icon = Icons.Outlined.BugReport, - onClick = onGetEventClick, - ) - Spacer(modifier = Modifier.height(8.dp)) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt index 7130911276..ecc4da5437 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt @@ -190,6 +190,7 @@ class ExpertModeViewModel @Inject constructor( fun onGetEventClick() { viewModelScope.launch { + if (warningState.value !is ExpertModeWarningState.Understood) return@launch navigate("get_event_debug", NavDestination.GetEvent) } } From bb6b4368ba357f7e1b27339e273b57c44a33ae7f Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 09:57:41 +0200 Subject: [PATCH 06/12] bump version to 4.2.1 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index 1d239ae17f..1813e00437 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.2.0 -VERSION_CODE=253 +VERSION_NAME=4.2.1 +VERSION_CODE=254 From 6a7a7520be78bdc6557e65fc08d7172e7c3fceaf Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 10:37:19 +0200 Subject: [PATCH 07/12] #2156 fix: do not throw an error when the Talkback application can not be found because there are many different package names out there Closes #2156 --- CHANGELOG.md | 1 + .../sds100/keymapper/base/actions/ActionErrorSnapshot.kt | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd65797b61..0feee9d04e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged. - #2074 Scrolling the action or trigger list no longer accidentally moves items; reordering by drag now only activates from the drag handle or via long-press. +- #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there. ## Changed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt index 4dcc63aa74..6acbd9374d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt @@ -49,6 +49,7 @@ class LazyActionErrorSnapshot( cameraAdapter, permissionAdapter, ) { + private val keyMapperImeHelper = KeyMapperImeHelper(switchImeInterface, inputMethodAdapter, buildConfigProvider.packageName) @@ -265,10 +266,6 @@ class LazyActionErrorSnapshot( } } - is ActionData.TalkBackGesture -> { - return getAppError(TALKBACK_PACKAGE_NAME) - } - else -> {} } @@ -321,5 +318,3 @@ interface ActionErrorSnapshot { fun getError(action: ActionData): KMError? fun getErrors(actions: List): Map } - -private const val TALKBACK_PACKAGE_NAME = "com.google.android.marvin.talkback" From 8816dd9170aca97a9818217e496147f020c6324b Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 10:45:22 +0200 Subject: [PATCH 08/12] fix changelog --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0feee9d04e..1547eebca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [4.2.1](https://github.com/sds100/KeyMapper/releases/tag/v4.2.1) + +#### 09 June 2026 + +## Fixed + +- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged. +- #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there. + ## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0) #### 03 June 2026 @@ -8,9 +17,7 @@ ## Fixed -- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged. - #2074 Scrolling the action or trigger list no longer accidentally moves items; reordering by drag now only activates from the drag handle or via long-press. -- #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there. ## Changed From aed7ca324f53f6aff24355b375b7bbd090cb4e33 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 09:09:07 +0000 Subject: [PATCH 09/12] #2157 feat: use settings ContentProvider via system bridge for choose setting screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the system bridge is connected (expert mode), the choose setting screen now queries the Settings ContentProvider directly through a new getAllSettings() AIDL method instead of falling back to the restricted ContentProvider query from the app process. The system bridge runs with shell/root privileges, giving it access to all settings rows that the app process cannot see. A queryCompat helper is added to IContentProviderUtils to handle the version-dependent IContentProvider.query() signatures across API 29–34+. When the system bridge is not connected, the existing ContentResolver query is used as a fallback. --- CHANGELOG.md | 1 + .../base/actions/ChooseSettingViewModel.kt | 39 +++++++++++- .../keymapper/sysbridge/ISystemBridge.aidl | 7 +++ .../sysbridge/service/SystemBridge.kt | 63 +++++++++++++++++++ .../sysbridge/utils/IContentProviderUtils.kt | 26 ++++++++ 5 files changed, 135 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7299123be3..a2cb693525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged. - #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there. - #2153 Prevent Direct Boot startup from initializing credential-encrypted app storage before the user unlocks. +- #2157 The "choose setting" screen now uses `settings list` via the system bridge when expert mode is active, surfacing all device settings instead of only those visible through the ContentProvider. ## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt index 2e3c343042..e8e7ad3522 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt @@ -7,6 +7,9 @@ import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.isConnected import io.github.sds100.keymapper.system.settings.SettingType import io.github.sds100.keymapper.system.settings.SettingsAdapter import javax.inject.Inject @@ -18,12 +21,14 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @HiltViewModel class ChooseSettingViewModel @Inject constructor( private val settingsAdapter: SettingsAdapter, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, @@ -36,7 +41,7 @@ class ChooseSettingViewModel @Inject constructor( val selectedSettingType = MutableStateFlow(SettingType.SYSTEM) val settings: StateFlow>> = combine(selectedSettingType, searchQuery) { type, query -> - val allSettings = settingsAdapter.getAll(type) + val allSettings = getSettings(type) val items = allSettings .filter { (key, _) -> query == null || key.contains(query, ignoreCase = true) } @@ -46,6 +51,38 @@ class ChooseSettingViewModel @Inject constructor( }.flowOn(Dispatchers.Default) .stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + private suspend fun getSettings(type: SettingType): Map { + if (systemBridgeConnectionManager.isConnected()) { + val namespace = when (type) { + SettingType.SYSTEM -> "system" + SettingType.SECURE -> "secure" + SettingType.GLOBAL -> "global" + } + val result = runInterruptible(Dispatchers.IO) { + systemBridgeConnectionManager.run { bridge -> + bridge.getAllSettings(namespace) + } + } + if (result is Success && result.value.isNotEmpty()) { + return parseKeyValuePairs(result.value) + } + } + return settingsAdapter.getAll(type) + } + + private fun parseKeyValuePairs(pairs: Array): Map { + val settings = sortedMapOf() + for (entry in pairs) { + val eqIdx = entry.indexOf('=') + if (eqIdx < 0) continue + val key = entry.substring(0, eqIdx) + if (key.isBlank()) continue + val value = if (eqIdx < entry.length - 1) entry.substring(eqIdx + 1) else null + settings[key] = value + } + return settings + } + fun onNavigateBack() { viewModelScope.launch { popBackStack() diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index ee04b124c8..8477527153 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -53,4 +53,11 @@ interface ISystemBridge { void registerLogCallback(ILogCallback callback) = 23; void unregisterLogCallback() = 24; void setLogLevel(int level) = 25; + + /** + * Returns all settings for the given namespace as an array of "key=value" strings. + * The namespace must be one of "system", "secure", or "global". + * Queries the Settings ContentProvider directly with the system bridge's elevated privileges. + */ + String[] getAllSettings(String namespace) = 26; } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 888958cc41..ee375ebf9a 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -903,4 +903,67 @@ class SystemBridge : ISystemBridge.Stub() { override fun setLogLevel(level: Int) { setLogLevelNative(level) } + + override fun getAllSettings(namespace: String?): Array { + namespace ?: return emptyArray() + + val settingsUri = when (namespace) { + "system" -> android.provider.Settings.System.CONTENT_URI + "secure" -> android.provider.Settings.Secure.CONTENT_URI + "global" -> android.provider.Settings.Global.CONTENT_URI + else -> return emptyArray() + } + + val authority = "settings" + val token: android.os.IBinder? = null + val userId = UserHandleUtils.getCallingUserId() + var provider: android.content.IContentProvider? = null + + try { + provider = ActivityManagerApis.getContentProviderExternal( + authority, + userId, + token, + authority, + ) + + if (provider == null) { + Log.w(TAG, "getAllSettings: Settings content provider is null for namespace=$namespace") + return emptyArray() + } + + val cursor = IContentProviderUtils.queryCompat( + provider, + processPackageName, + settingsUri, + arrayOf("name", "value"), + null, + ) ?: return emptyArray() + + val results = mutableListOf() + cursor.use { + val nameIndex = it.getColumnIndex("name") + val valueIndex = it.getColumnIndex("value") + if (nameIndex >= 0) { + while (it.moveToNext()) { + val name = it.getString(nameIndex) ?: continue + val value = if (valueIndex >= 0) it.getString(valueIndex) else null + results.add("$name=${value ?: ""}") + } + } + } + return results.toTypedArray() + } catch (e: Exception) { + Log.e(TAG, "getAllSettings: Failed to query settings for namespace=$namespace", e) + return emptyArray() + } finally { + if (provider != null) { + try { + ActivityManagerApis.removeContentProviderExternal(authority, token) + } catch (tr: Throwable) { + Log.w(TAG, "getAllSettings: Failed to remove content provider", tr) + } + } + } + } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt index d83d20744d..23ded26d92 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt @@ -2,6 +2,8 @@ package io.github.sds100.keymapper.sysbridge.utils import android.content.AttributionSource import android.content.IContentProvider +import android.database.Cursor +import android.net.Uri import android.os.Build import android.os.Bundle @@ -38,4 +40,28 @@ internal object IContentProviderUtils { return result } + + @Throws(android.os.RemoteException::class) + fun queryCompat( + provider: IContentProvider, + callingPkg: String?, + url: Uri, + projection: Array?, + queryArgs: Bundle?, + ): Cursor? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val uid = android.system.Os.getuid() + provider.query( + AttributionSource.Builder(uid).setPackageName(callingPkg).build(), + url, + projection, + queryArgs, + null, + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + provider.query(callingPkg, null as String?, url, projection, queryArgs, null) + } else { + provider.query(callingPkg, url, projection, queryArgs, null) + } + } } From 1b2a0771c663cf7b46f1e1c6bf204d2efe0a3bd7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 11:11:38 +0200 Subject: [PATCH 10/12] cleanup code --- .../keymapper/sysbridge/service/SystemBridge.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index ee375ebf9a..652135af7e 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -481,7 +481,7 @@ class SystemBridge : ISystemBridge.Stub() { override fun setGrabTargets( devices: Array?, - ): Array? { + ): Array { return setGrabTargetsNative(devices?.filterNotNull()?.toTypedArray() ?: emptyArray()) } @@ -915,9 +915,9 @@ class SystemBridge : ISystemBridge.Stub() { } val authority = "settings" - val token: android.os.IBinder? = null + val token: IBinder? = null val userId = UserHandleUtils.getCallingUserId() - var provider: android.content.IContentProvider? = null + var provider: IContentProvider? = null try { provider = ActivityManagerApis.getContentProviderExternal( @@ -928,7 +928,10 @@ class SystemBridge : ISystemBridge.Stub() { ) if (provider == null) { - Log.w(TAG, "getAllSettings: Settings content provider is null for namespace=$namespace") + Log.w( + TAG, + "getAllSettings: Settings content provider is null for namespace=$namespace", + ) return emptyArray() } From bf4b09b910518bec7c4d9a1981d00277a51d4091 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 11:26:59 +0200 Subject: [PATCH 11/12] add proguard rules for android.os.ICancellationSignal --- app/proguard-rules.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 1e905036f2..0f7be6ae53 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -162,6 +162,7 @@ # android.content package classes -keep class android.content.IContentProvider** { *; } -keep class android.content.IIntentReceiver** { *; } +-keep class android.os.ICancellationSignal** { *; } # android.content.pm package classes -keep class android.content.pm.IPackageManager** { *; } @@ -245,3 +246,4 @@ -dontwarn android.view.IWindowManager** -dontwarn com.android.internal.app.** -dontwarn com.android.internal.policy.** +-dontwarn android.os.ICancellationSignal \ No newline at end of file From 77eb149eb9b417308f6fc52a3aa8dc9be1ea4193 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 11 Jun 2026 10:47:29 +0200 Subject: [PATCH 12/12] 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 1813e00437..0e1183310c 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ VERSION_NAME=4.2.1 -VERSION_CODE=254 +VERSION_CODE=256