diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4ca051484..a2cb693525 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+## [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.
+- #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)
#### 03 June 2026
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
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/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 {
diff --git a/app/version.properties b/app/version.properties
index 1d239ae17f..0e1183310c 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=256
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
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..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
@@ -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/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"
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/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)
}
}
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..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,6 +27,7 @@ import androidx.lifecycle.LifecycleRegistry
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
+import dagger.Lazy
import dagger.hilt.android.AndroidEntryPoint
import io.github.sds100.keymapper.base.R
import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType
@@ -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? =
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..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())
}
@@ -903,4 +903,70 @@ 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: IBinder? = null
+ val userId = UserHandleUtils.getCallingUserId()
+ var provider: 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)
+ }
+ }
}