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/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/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..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) + } + } }