Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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** { *; }
Expand Down Expand Up @@ -245,3 +246,4 @@
-dontwarn android.view.IWindowManager**
-dontwarn com.android.internal.app.**
-dontwarn com.android.internal.policy.**
-dontwarn android.os.ICancellationSignal
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -36,7 +41,7 @@ class ChooseSettingViewModel @Inject constructor(
val selectedSettingType = MutableStateFlow(SettingType.SYSTEM)
val settings: StateFlow<State<List<SettingItem>>> =
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) }
Expand All @@ -46,6 +51,38 @@ class ChooseSettingViewModel @Inject constructor(
}.flowOn(Dispatchers.Default)
.stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading)

private suspend fun getSettings(type: SettingType): Map<String, String?> {
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<String>): Map<String, String?> {
val settings = sortedMapOf<String, String?>()
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ class SystemBridge : ISystemBridge.Stub() {

override fun setGrabTargets(
devices: Array<out GrabTargetKeyCode?>?,
): Array<out GrabbedDeviceHandle?>? {
): Array<out GrabbedDeviceHandle?> {
return setGrabTargetsNative(devices?.filterNotNull()?.toTypedArray() ?: emptyArray())
}

Expand Down Expand Up @@ -903,4 +903,70 @@ class SystemBridge : ISystemBridge.Stub() {
override fun setLogLevel(level: Int) {
setLogLevelNative(level)
}

override fun getAllSettings(namespace: String?): Array<String> {
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<String>()
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)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -38,4 +40,28 @@ internal object IContentProviderUtils {

return result
}

@Throws(android.os.RemoteException::class)
fun queryCompat(
provider: IContentProvider,
callingPkg: String?,
url: Uri,
projection: Array<String>?,
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)
}
}
}
Loading