Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [4.0.0 Beta 3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.03)

#### TO BE RELEASED

## Added
- #1871 action to modify any system settings

## [4.0.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.02)

#### 08 November 2025
Expand Down
1 change: 1 addition & 0 deletions base/src/main/assets/whats-new.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ You can now remap ALL buttons when the screen is off (including the power button
• Send SMS messages
• Force stop current app or clear from recents
• Mute/unmute microphone
• Modify any system setting

🆕 New Features
• Redesigned Settings screen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStateAtLeast
import androidx.navigation.findNavController
import com.anggrayudi.storage.extension.openInputStream
import com.anggrayudi.storage.extension.openOutputStream
import com.anggrayudi.storage.extension.toDocumentFile
Expand All @@ -32,6 +31,7 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase
import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl
import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate
import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl
import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl
import io.github.sds100.keymapper.common.BuildConfigProvider
import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl
Expand All @@ -43,12 +43,12 @@ import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapt
import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter
import io.github.sds100.keymapper.system.root.SuAdapterImpl
import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

abstract class BaseMainActivity : AppCompatActivity() {

Expand Down Expand Up @@ -105,6 +105,9 @@ abstract class BaseMainActivity : AppCompatActivity() {
@Inject
lateinit var inputEventHub: InputEventHubImpl

@Inject
lateinit var navigationProvider: NavigationProvider

private lateinit var requestPermissionDelegate: RequestPermissionDelegate

private val currentNightMode: Int
Expand Down Expand Up @@ -162,15 +165,14 @@ abstract class BaseMainActivity : AppCompatActivity() {
notificationReceiverAdapter = notificationReceiverAdapter,
buildConfigProvider = buildConfigProvider,
shizukuAdapter = shizukuAdapter,
navigationProvider = navigationProvider,
coroutineScope = lifecycleScope,
)

permissionAdapter.request
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { permission ->
requestPermissionDelegate.requestPermission(
permission,
findNavController(R.id.container),
)
requestPermissionDelegate.requestPermission(permission)
}
.launchIn(lifecycleScope)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import io.github.sds100.keymapper.base.actions.ChooseActionScreen
import io.github.sds100.keymapper.base.actions.ChooseActionViewModel
import io.github.sds100.keymapper.base.actions.ChooseSettingScreen
import io.github.sds100.keymapper.base.actions.ConfigShellCommandViewModel
import io.github.sds100.keymapper.base.actions.ShellCommandActionScreen
import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementScreen
Expand Down Expand Up @@ -164,6 +165,13 @@ fun BaseMainNavHost(
)
}

composable<NavDestination.ChooseSetting> {
ChooseSettingScreen(
modifier = Modifier.fillMaxSize(),
viewModel = hiltViewModel(),
)
}

composableDestinations()
}
}
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 @@ -22,6 +22,7 @@ import io.github.sds100.keymapper.system.camera.CameraLens
import io.github.sds100.keymapper.system.intents.IntentExtraModel
import io.github.sds100.keymapper.system.intents.IntentTarget
import io.github.sds100.keymapper.system.network.HttpMethod
import io.github.sds100.keymapper.system.settings.SettingType
import io.github.sds100.keymapper.system.volume.DndMode
import io.github.sds100.keymapper.system.volume.RingerMode
import io.github.sds100.keymapper.system.volume.VolumeStream
Expand Down Expand Up @@ -50,6 +51,7 @@ object ActionDataEntityMapper {

ActionEntity.Type.INTERACT_UI_ELEMENT -> ActionId.INTERACT_UI_ELEMENT
ActionEntity.Type.SHELL_COMMAND -> ActionId.SHELL_COMMAND
ActionEntity.Type.MODIFY_SETTING -> ActionId.MODIFY_SETTING
}

return when (actionId) {
Expand Down Expand Up @@ -723,6 +725,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 {
SettingType.valueOf(settingTypeString)
} catch (_: IllegalArgumentException) {
SettingType.SYSTEM
}

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

Expand All @@ -749,6 +771,7 @@ object ActionDataEntityMapper {
is ActionData.Sound -> ActionEntity.Type.SOUND
is ActionData.InteractUiElement -> ActionEntity.Type.INTERACT_UI_ELEMENT
is ActionData.ShellCommand -> ActionEntity.Type.SHELL_COMMAND
is ActionData.ModifySetting -> ActionEntity.Type.MODIFY_SETTING
else -> ActionEntity.Type.SYSTEM_ACTION
}

Expand Down Expand Up @@ -825,6 +848,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 +1129,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 +1308,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 @@ -27,6 +27,7 @@ import io.github.sds100.keymapper.system.permissions.Permission
import io.github.sds100.keymapper.system.permissions.PermissionAdapter
import io.github.sds100.keymapper.system.permissions.SystemFeatureAdapter
import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter
import io.github.sds100.keymapper.system.settings.SettingType

class LazyActionErrorSnapshot(
private val packageManager: PackageManagerAdapter,
Expand Down Expand Up @@ -231,6 +232,27 @@ class LazyActionErrorSnapshot(
}
}

is ActionData.ModifySetting -> {
return when (action.settingType) {
SettingType.SYSTEM -> {
if (!isPermissionGranted(Permission.WRITE_SETTINGS)) {
SystemError.PermissionDenied(Permission.WRITE_SETTINGS)
} else {
null
}
}
SettingType.SECURE,
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 @@ -24,14 +24,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
Expand Down Expand Up @@ -413,8 +411,8 @@ private fun Preview() {
KeyMapperTheme {
val sheetState = SheetState(
skipPartiallyExpanded = true,
density = LocalDensity.current,
initialValue = Expanded,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
)

ActionOptionsBottomSheet(
Expand Down Expand Up @@ -472,8 +470,8 @@ private fun PreviewNoEditButton() {
KeyMapperTheme {
val sheetState = SheetState(
skipPartiallyExpanded = true,
density = LocalDensity.current,
initialValue = Expanded,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
)

ActionOptionsBottomSheet(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,13 @@ 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 -> {
getString(
R.string.modify_setting_description,
arrayOf(action.settingKey, action.value),
)
}
}

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,9 @@ object ActionUtils {
return listOf(Permission.FIND_NEARBY_DEVICES)
}

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

else -> return emptyList()
}

Expand Down Expand Up @@ -890,6 +896,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 Expand Up @@ -936,6 +944,7 @@ fun ActionData.isEditable(): Boolean = when (this) {
is ActionData.ShellCommand,
is ActionData.InteractUiElement,
is ActionData.MoveCursor,
is ActionData.ModifySetting,
-> true

else -> false
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
Loading
Loading