Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4e0e707
Initial plan
Copilot Oct 31, 2025
841feaa
Add core implementation for modify system settings actions
Copilot Oct 31, 2025
d0d21d3
Add UI bottom sheet for modifying system settings
Copilot Oct 31, 2025
4cc6ed9
Add action title and description display for modify settings
Copilot Oct 31, 2025
6237188
Fix SystemBridge settings implementation with proper ContentResolver …
Copilot Oct 31, 2025
927a3d6
Refactor to use single ActionId with SettingType enum
Copilot Oct 31, 2025
51b18eb
Refactor settings modification to use SettingsAdapter and permissions
Copilot Oct 31, 2025
b57cdf8
Add ChooseSettingScreen and ViewModel for setting selection
Copilot Oct 31, 2025
c1f999f
Fix code review feedback: remove fully qualified names and use KeyMap…
Copilot Oct 31, 2025
c351c61
Refactor SettingsAdapter and ModifySettingActionBottomSheet per review
Copilot Oct 31, 2025
67d4efd
Merge branch 'develop' into copilot/modify-system-settings-permission
sds100 Nov 1, 2025
b149abf
Consolidate SettingsAdapter methods and import SettingType in ActionE…
Copilot Nov 2, 2025
e441a04
Merge branch 'develop' into copilot/modify-system-settings-permission
sds100 Nov 7, 2025
162a959
#1871 clean up modify settings bottom sheet
sds100 Nov 7, 2025
16ef439
#1871 use segmented buttons to switch setting type
sds100 Nov 7, 2025
1862d3b
#1871 clean up ChooseSettingScreen.kt and add previews
sds100 Nov 8, 2025
a75ea9f
#1871 ModifySetting action is now editable
sds100 Nov 8, 2025
8149230
#1871 add backhandler to ChooseSettingScreen
sds100 Nov 8, 2025
95d22d0
#1871 fix dismissing ModifySettingActionBottomSheet
sds100 Nov 8, 2025
2b0efdc
chore: upgrade compose BOM and navigation libraries
sds100 Nov 8, 2025
0f168ad
#1871 use monospace font in ChooseSettingScreen.kt
sds100 Nov 8, 2025
94b77f2
#1871 fix saving ModifySettings action
sds100 Nov 8, 2025
f300bf8
#1871 add TODO
sds100 Nov 8, 2025
9a92925
Merge branch 'develop' into copilot/modify-system-settings-permission
sds100 Nov 9, 2025
302095d
feat: WRITE_SECURE_SETTINGS permission dialog now directs the user to…
sds100 Nov 9, 2025
ca8951d
#1871 feat: complete modify system setting action
sds100 Nov 9, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -949,4 +949,24 @@ sealed class ActionData : Comparable<ActionData> {
data object ClearRecentApp : ActionData() {
override val id: ActionId = ActionId.CLEAR_RECENT_APP
}

@Serializable
data class ModifySetting(
val settingType: io.github.sds100.keymapper.system.settings.SettingType,
val settingKey: String,
val value: String,
) : ActionData() {
override val id: ActionId = ActionId.MODIFY_SETTING

override fun compareTo(other: ActionData) = when (other) {
is ModifySetting -> compareValuesBy(
this,
other,
{ it.settingType },
{ it.settingKey },
{ it.value },
)
else -> super.compareTo(other)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,26 @@ object ActionDataEntityMapper {

ActionId.FORCE_STOP_APP -> ActionData.ForceStopApp
ActionId.CLEAR_RECENT_APP -> ActionData.ClearRecentApp

ActionId.MODIFY_SETTING -> {
val value = entity.extras.getData(ActionEntity.EXTRA_SETTING_VALUE)
.valueOrNull() ?: return null

val settingTypeString = entity.extras.getData(ActionEntity.EXTRA_SETTING_TYPE)
.valueOrNull() ?: "SYSTEM" // Default to SYSTEM for backward compatibility

val settingType = try {
io.github.sds100.keymapper.system.settings.SettingType.valueOf(settingTypeString)
} catch (e: IllegalArgumentException) {
io.github.sds100.keymapper.system.settings.SettingType.SYSTEM
}

ActionData.ModifySetting(
settingType = settingType,
settingKey = entity.data,
value = value,
)
}
}
}

Expand Down Expand Up @@ -825,6 +845,7 @@ object ActionDataEntityMapper {
is ActionData.ControlMedia.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!!
is ActionData.ControlMedia.Stop -> SYSTEM_ACTION_ID_MAP[data.id]!!
is ActionData.GoBack -> SYSTEM_ACTION_ID_MAP[data.id]!!
is ActionData.ModifySetting -> data.settingKey
else -> SYSTEM_ACTION_ID_MAP[data.id]!!
}

Expand Down Expand Up @@ -1105,6 +1126,11 @@ object ActionDataEntityMapper {
EntityExtra(ActionEntity.EXTRA_SHELL_COMMAND_TIMEOUT, data.timeoutMillis.toString()),
)

is ActionData.ModifySetting -> listOf(
EntityExtra(ActionEntity.EXTRA_SETTING_VALUE, data.value),
EntityExtra(ActionEntity.EXTRA_SETTING_TYPE, data.settingType.name),
)

else -> emptyList()
}

Expand Down Expand Up @@ -1279,5 +1305,7 @@ object ActionDataEntityMapper {
ActionId.HTTP_REQUEST to "http_request",
ActionId.FORCE_STOP_APP to "force_stop_app",
ActionId.CLEAR_RECENT_APP to "clear_recent_app",

ActionId.MODIFY_SETTING to "modify_setting",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,26 @@ class LazyActionErrorSnapshot(
}
}

is ActionData.ModifySetting -> {
return when (action.settingType) {
io.github.sds100.keymapper.system.settings.SettingType.SYSTEM -> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import SettingType

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b149abf. Now importing SettingType and using it throughout the file instead of fully qualified name.

if (!isPermissionGranted(Permission.WRITE_SETTINGS)) {
SystemError.PermissionDenied(Permission.WRITE_SETTINGS)
} else {
null
}
}
io.github.sds100.keymapper.system.settings.SettingType.SECURE,
io.github.sds100.keymapper.system.settings.SettingType.GLOBAL -> {
if (!isPermissionGranted(Permission.WRITE_SECURE_SETTINGS)) {
SystemError.PermissionDenied(Permission.WRITE_SECURE_SETTINGS)
} else {
null
}
}
}
}

else -> {}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,6 @@ enum class ActionId {

FORCE_STOP_APP,
CLEAR_RECENT_APP,

MODIFY_SETTING,
}
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,21 @@ class ActionUiHelper(
ActionData.Microphone.Mute -> getString(R.string.action_mute_microphone)
ActionData.Microphone.Toggle -> getString(R.string.action_toggle_mute_microphone)
ActionData.Microphone.Unmute -> getString(R.string.action_unmute_microphone)

is ActionData.ModifySetting -> {
val typeString = when (action.settingType) {
io.github.sds100.keymapper.system.settings.SettingType.SYSTEM ->
getString(R.string.modify_setting_type_system)
io.github.sds100.keymapper.system.settings.SettingType.SECURE ->
getString(R.string.modify_setting_type_secure)
io.github.sds100.keymapper.system.settings.SettingType.GLOBAL ->
getString(R.string.modify_setting_type_global)
}
getString(
R.string.modify_setting_description,
arrayOf(action.settingKey, action.value, typeString),
)
}
}

fun getIcon(action: ActionData): ComposeIconInfo = when (action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ object ActionUtils {
ActionId.INTERACT_UI_ELEMENT -> ActionCategory.APPS
ActionId.FORCE_STOP_APP -> ActionCategory.APPS
ActionId.CLEAR_RECENT_APP -> ActionCategory.APPS
ActionId.MODIFY_SETTING -> ActionCategory.APPS

ActionId.CONSUME_KEY_EVENT -> ActionCategory.SPECIAL
}
Expand Down Expand Up @@ -383,6 +384,8 @@ object ActionUtils {
ActionId.INTERACT_UI_ELEMENT -> R.string.action_interact_ui_element_title
ActionId.FORCE_STOP_APP -> R.string.action_force_stop_app
ActionId.CLEAR_RECENT_APP -> R.string.action_clear_recent_app

ActionId.MODIFY_SETTING -> R.string.action_modify_setting
}

@DrawableRes
Expand Down Expand Up @@ -760,6 +763,8 @@ object ActionUtils {
return listOf(Permission.FIND_NEARBY_DEVICES)
}

ActionId.MODIFY_SETTING -> return emptyList() // Permissions handled based on setting type at runtime

else -> return emptyList()
}

Expand Down Expand Up @@ -890,6 +895,8 @@ object ActionUtils {
ActionId.INTERACT_UI_ELEMENT -> KeyMapperIcons.JumpToElement
ActionId.FORCE_STOP_APP -> Icons.Outlined.Dangerous
ActionId.CLEAR_RECENT_APP -> Icons.Outlined.VerticalSplit

ActionId.MODIFY_SETTING -> Icons.Outlined.Settings
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ fun HandleActionBottomSheets(delegate: CreateActionDelegate) {
HttpRequestBottomSheet(delegate)
SmsActionBottomSheet(delegate)
VolumeActionBottomSheet(delegate)
ModifySettingActionBottomSheet(delegate)
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package io.github.sds100.keymapper.base.actions

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.github.sds100.keymapper.base.R
import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperDropdownMenu
import io.github.sds100.keymapper.base.utils.ui.compose.SearchAppBarActions
import io.github.sds100.keymapper.common.utils.State
import io.github.sds100.keymapper.system.settings.SettingType
import kotlinx.coroutines.flow.update

@Composable
fun ChooseSettingScreen(modifier: Modifier = Modifier, viewModel: ChooseSettingViewModel) {
val state by viewModel.settings.collectAsStateWithLifecycle()
val query by viewModel.searchQuery.collectAsStateWithLifecycle()
val settingType by viewModel.selectedSettingType.collectAsStateWithLifecycle()

ChooseSettingScreen(
modifier = modifier,
state = state,
query = query,
settingType = settingType,
onQueryChange = { newQuery -> viewModel.searchQuery.update { newQuery } },
onCloseSearch = { viewModel.searchQuery.update { null } },
onSettingTypeChange = { viewModel.selectedSettingType.value = it },
onClickSetting = viewModel::onSettingClick,
onNavigateBack = viewModel::onNavigateBack,
)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ChooseSettingScreen(
modifier: Modifier = Modifier,
state: State<List<SettingItem>>,
query: String? = null,
settingType: SettingType,
onQueryChange: (String) -> Unit = {},
onCloseSearch: () -> Unit = {},
onSettingTypeChange: (SettingType) -> Unit = {},
onClickSetting: (String, String?) -> Unit = { _, _ -> },
onNavigateBack: () -> Unit = {},
) {
Scaffold(
modifier = modifier.displayCutoutPadding(),
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.choose_setting_title)) },
)
},
bottomBar = {
BottomAppBar(
modifier = Modifier.imePadding(),
actions = {
SearchAppBarActions(
onCloseSearch = onCloseSearch,
onNavigateBack = onNavigateBack,
onQueryChange = onQueryChange,
enabled = state is State.Data,
query = query,
)
},
)
},
) { innerPadding ->
Surface(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
Column(modifier = Modifier.fillMaxSize()) {
// Setting type dropdown
var expanded by remember { mutableStateOf(false) }

KeyMapperDropdownMenu(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
expanded = expanded,
onExpandedChange = { expanded = it },
label = { Text(stringResource(R.string.modify_setting_type_label)) },
selectedValue = settingType,
values = listOf(
SettingType.SYSTEM to stringResource(R.string.modify_setting_type_system),
SettingType.SECURE to stringResource(R.string.modify_setting_type_secure),
SettingType.GLOBAL to stringResource(R.string.modify_setting_type_global),
),
onValueChanged = onSettingTypeChange,
)

HorizontalDivider()

// Settings list
when (state) {
State.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is State.Data -> {
if (state.data.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = stringResource(R.string.choose_setting_empty),
style = MaterialTheme.typography.bodyLarge,
)
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(state.data) { item ->
ListItem(
headlineContent = { Text(item.key) },
supportingContent = item.value?.let { { Text(it) } },
modifier = Modifier.clickable {
onClickSetting(item.key, item.value)
},
)
HorizontalDivider()
}
}
}
}
}
}
}
}
}
Loading
Loading