Skip to content

Commit 9c6aeb2

Browse files
authored
Merge pull request #2159 from keymapperorg/claude/issue-2157-settings-list-expert-mode
Listing settings for modify setting action should use `settings list` for expert mode users
2 parents c88dec4 + bf4b09b commit 9c6aeb2

6 files changed

Lines changed: 141 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged.
88
- #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there.
99
- #2153 Prevent Direct Boot startup from initializing credential-encrypted app storage before the user unlocks.
10+
- #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.
1011

1112
## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0)
1213

app/proguard-rules.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
# android.content package classes
163163
-keep class android.content.IContentProvider** { *; }
164164
-keep class android.content.IIntentReceiver** { *; }
165+
-keep class android.os.ICancellationSignal** { *; }
165166

166167
# android.content.pm package classes
167168
-keep class android.content.pm.IPackageManager** { *; }
@@ -245,3 +246,4 @@
245246
-dontwarn android.view.IWindowManager**
246247
-dontwarn com.android.internal.app.**
247248
-dontwarn com.android.internal.policy.**
249+
-dontwarn android.os.ICancellationSignal

base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
77
import io.github.sds100.keymapper.base.utils.ui.DialogProvider
88
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
99
import io.github.sds100.keymapper.common.utils.State
10+
import io.github.sds100.keymapper.common.utils.Success
11+
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
12+
import io.github.sds100.keymapper.sysbridge.manager.isConnected
1013
import io.github.sds100.keymapper.system.settings.SettingType
1114
import io.github.sds100.keymapper.system.settings.SettingsAdapter
1215
import javax.inject.Inject
@@ -18,12 +21,14 @@ import kotlinx.coroutines.flow.combine
1821
import kotlinx.coroutines.flow.flowOn
1922
import kotlinx.coroutines.flow.stateIn
2023
import kotlinx.coroutines.launch
24+
import kotlinx.coroutines.runInterruptible
2125
import kotlinx.serialization.Serializable
2226
import kotlinx.serialization.json.Json
2327

2428
@HiltViewModel
2529
class ChooseSettingViewModel @Inject constructor(
2630
private val settingsAdapter: SettingsAdapter,
31+
private val systemBridgeConnectionManager: SystemBridgeConnectionManager,
2732
resourceProvider: ResourceProvider,
2833
navigationProvider: NavigationProvider,
2934
dialogProvider: DialogProvider,
@@ -36,7 +41,7 @@ class ChooseSettingViewModel @Inject constructor(
3641
val selectedSettingType = MutableStateFlow(SettingType.SYSTEM)
3742
val settings: StateFlow<State<List<SettingItem>>> =
3843
combine(selectedSettingType, searchQuery) { type, query ->
39-
val allSettings = settingsAdapter.getAll(type)
44+
val allSettings = getSettings(type)
4045

4146
val items = allSettings
4247
.filter { (key, _) -> query == null || key.contains(query, ignoreCase = true) }
@@ -46,6 +51,38 @@ class ChooseSettingViewModel @Inject constructor(
4651
}.flowOn(Dispatchers.Default)
4752
.stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading)
4853

54+
private suspend fun getSettings(type: SettingType): Map<String, String?> {
55+
if (systemBridgeConnectionManager.isConnected()) {
56+
val namespace = when (type) {
57+
SettingType.SYSTEM -> "system"
58+
SettingType.SECURE -> "secure"
59+
SettingType.GLOBAL -> "global"
60+
}
61+
val result = runInterruptible(Dispatchers.IO) {
62+
systemBridgeConnectionManager.run { bridge ->
63+
bridge.getAllSettings(namespace)
64+
}
65+
}
66+
if (result is Success && result.value.isNotEmpty()) {
67+
return parseKeyValuePairs(result.value)
68+
}
69+
}
70+
return settingsAdapter.getAll(type)
71+
}
72+
73+
private fun parseKeyValuePairs(pairs: Array<String>): Map<String, String?> {
74+
val settings = sortedMapOf<String, String?>()
75+
for (entry in pairs) {
76+
val eqIdx = entry.indexOf('=')
77+
if (eqIdx < 0) continue
78+
val key = entry.substring(0, eqIdx)
79+
if (key.isBlank()) continue
80+
val value = if (eqIdx < entry.length - 1) entry.substring(eqIdx + 1) else null
81+
settings[key] = value
82+
}
83+
return settings
84+
}
85+
4986
fun onNavigateBack() {
5087
viewModelScope.launch {
5188
popBackStack()

sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,11 @@ interface ISystemBridge {
5353
void registerLogCallback(ILogCallback callback) = 23;
5454
void unregisterLogCallback() = 24;
5555
void setLogLevel(int level) = 25;
56+
57+
/**
58+
* Returns all settings for the given namespace as an array of "key=value" strings.
59+
* The namespace must be one of "system", "secure", or "global".
60+
* Queries the Settings ContentProvider directly with the system bridge's elevated privileges.
61+
*/
62+
String[] getAllSettings(String namespace) = 26;
5663
}

sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ class SystemBridge : ISystemBridge.Stub() {
481481

482482
override fun setGrabTargets(
483483
devices: Array<out GrabTargetKeyCode?>?,
484-
): Array<out GrabbedDeviceHandle?>? {
484+
): Array<out GrabbedDeviceHandle?> {
485485
return setGrabTargetsNative(devices?.filterNotNull()?.toTypedArray() ?: emptyArray())
486486
}
487487

@@ -903,4 +903,70 @@ class SystemBridge : ISystemBridge.Stub() {
903903
override fun setLogLevel(level: Int) {
904904
setLogLevelNative(level)
905905
}
906+
907+
override fun getAllSettings(namespace: String?): Array<String> {
908+
namespace ?: return emptyArray()
909+
910+
val settingsUri = when (namespace) {
911+
"system" -> android.provider.Settings.System.CONTENT_URI
912+
"secure" -> android.provider.Settings.Secure.CONTENT_URI
913+
"global" -> android.provider.Settings.Global.CONTENT_URI
914+
else -> return emptyArray()
915+
}
916+
917+
val authority = "settings"
918+
val token: IBinder? = null
919+
val userId = UserHandleUtils.getCallingUserId()
920+
var provider: IContentProvider? = null
921+
922+
try {
923+
provider = ActivityManagerApis.getContentProviderExternal(
924+
authority,
925+
userId,
926+
token,
927+
authority,
928+
)
929+
930+
if (provider == null) {
931+
Log.w(
932+
TAG,
933+
"getAllSettings: Settings content provider is null for namespace=$namespace",
934+
)
935+
return emptyArray()
936+
}
937+
938+
val cursor = IContentProviderUtils.queryCompat(
939+
provider,
940+
processPackageName,
941+
settingsUri,
942+
arrayOf("name", "value"),
943+
null,
944+
) ?: return emptyArray()
945+
946+
val results = mutableListOf<String>()
947+
cursor.use {
948+
val nameIndex = it.getColumnIndex("name")
949+
val valueIndex = it.getColumnIndex("value")
950+
if (nameIndex >= 0) {
951+
while (it.moveToNext()) {
952+
val name = it.getString(nameIndex) ?: continue
953+
val value = if (valueIndex >= 0) it.getString(valueIndex) else null
954+
results.add("$name=${value ?: ""}")
955+
}
956+
}
957+
}
958+
return results.toTypedArray()
959+
} catch (e: Exception) {
960+
Log.e(TAG, "getAllSettings: Failed to query settings for namespace=$namespace", e)
961+
return emptyArray()
962+
} finally {
963+
if (provider != null) {
964+
try {
965+
ActivityManagerApis.removeContentProviderExternal(authority, token)
966+
} catch (tr: Throwable) {
967+
Log.w(TAG, "getAllSettings: Failed to remove content provider", tr)
968+
}
969+
}
970+
}
971+
}
906972
}

sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package io.github.sds100.keymapper.sysbridge.utils
22

33
import android.content.AttributionSource
44
import android.content.IContentProvider
5+
import android.database.Cursor
6+
import android.net.Uri
57
import android.os.Build
68
import android.os.Bundle
79

@@ -38,4 +40,28 @@ internal object IContentProviderUtils {
3840

3941
return result
4042
}
43+
44+
@Throws(android.os.RemoteException::class)
45+
fun queryCompat(
46+
provider: IContentProvider,
47+
callingPkg: String?,
48+
url: Uri,
49+
projection: Array<String>?,
50+
queryArgs: Bundle?,
51+
): Cursor? {
52+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
53+
val uid = android.system.Os.getuid()
54+
provider.query(
55+
AttributionSource.Builder(uid).setPackageName(callingPkg).build(),
56+
url,
57+
projection,
58+
queryArgs,
59+
null,
60+
)
61+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
62+
provider.query(callingPkg, null as String?, url, projection, queryArgs, null)
63+
} else {
64+
provider.query(callingPkg, url, projection, queryArgs, null)
65+
}
66+
}
4167
}

0 commit comments

Comments
 (0)