Skip to content

Commit 7475f2c

Browse files
authored
Merge pull request #1872 from keymapperorg/copilot/modify-system-settings-permission
Add action to modify Android system settings via key mappings
2 parents 04328e3 + ca8951d commit 7475f2c

37 files changed

+1291
-142
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [4.0.0 Beta 3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.03)
2+
3+
#### TO BE RELEASED
4+
5+
## Added
6+
- #1871 action to modify any system settings
7+
18
## [4.0.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.02)
29

310
#### 08 November 2025

base/src/main/assets/whats-new.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ You can now remap ALL buttons when the screen is off (including the power button
66
• Send SMS messages
77
• Force stop current app or clear from recents
88
• Mute/unmute microphone
9+
• Modify any system setting
910

1011
🆕 New Features
1112
• Redesigned Settings screen

base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import androidx.lifecycle.Lifecycle
2121
import androidx.lifecycle.flowWithLifecycle
2222
import androidx.lifecycle.lifecycleScope
2323
import androidx.lifecycle.withStateAtLeast
24-
import androidx.navigation.findNavController
2524
import com.anggrayudi.storage.extension.openInputStream
2625
import com.anggrayudi.storage.extension.openOutputStream
2726
import com.anggrayudi.storage.extension.toDocumentFile
@@ -32,6 +31,7 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase
3231
import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl
3332
import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate
3433
import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl
34+
import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
3535
import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl
3636
import io.github.sds100.keymapper.common.BuildConfigProvider
3737
import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl
@@ -43,12 +43,12 @@ import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapt
4343
import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter
4444
import io.github.sds100.keymapper.system.root.SuAdapterImpl
4545
import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter
46-
import javax.inject.Inject
4746
import kotlinx.coroutines.Dispatchers
4847
import kotlinx.coroutines.flow.launchIn
4948
import kotlinx.coroutines.flow.onEach
5049
import kotlinx.coroutines.launch
5150
import timber.log.Timber
51+
import javax.inject.Inject
5252

5353
abstract class BaseMainActivity : AppCompatActivity() {
5454

@@ -105,6 +105,9 @@ abstract class BaseMainActivity : AppCompatActivity() {
105105
@Inject
106106
lateinit var inputEventHub: InputEventHubImpl
107107

108+
@Inject
109+
lateinit var navigationProvider: NavigationProvider
110+
108111
private lateinit var requestPermissionDelegate: RequestPermissionDelegate
109112

110113
private val currentNightMode: Int
@@ -162,15 +165,14 @@ abstract class BaseMainActivity : AppCompatActivity() {
162165
notificationReceiverAdapter = notificationReceiverAdapter,
163166
buildConfigProvider = buildConfigProvider,
164167
shizukuAdapter = shizukuAdapter,
168+
navigationProvider = navigationProvider,
169+
coroutineScope = lifecycleScope,
165170
)
166171

167172
permissionAdapter.request
168173
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
169174
.onEach { permission ->
170-
requestPermissionDelegate.requestPermission(
171-
permission,
172-
findNavController(R.id.container),
173-
)
175+
requestPermissionDelegate.requestPermission(permission)
174176
}
175177
.launchIn(lifecycleScope)
176178

base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.navigation.compose.NavHost
1818
import androidx.navigation.compose.composable
1919
import io.github.sds100.keymapper.base.actions.ChooseActionScreen
2020
import io.github.sds100.keymapper.base.actions.ChooseActionViewModel
21+
import io.github.sds100.keymapper.base.actions.ChooseSettingScreen
2122
import io.github.sds100.keymapper.base.actions.ConfigShellCommandViewModel
2223
import io.github.sds100.keymapper.base.actions.ShellCommandActionScreen
2324
import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementScreen
@@ -164,6 +165,13 @@ fun BaseMainNavHost(
164165
)
165166
}
166167

168+
composable<NavDestination.ChooseSetting> {
169+
ChooseSettingScreen(
170+
modifier = Modifier.fillMaxSize(),
171+
viewModel = hiltViewModel(),
172+
)
173+
}
174+
167175
composableDestinations()
168176
}
169177
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,4 +949,24 @@ sealed class ActionData : Comparable<ActionData> {
949949
data object ClearRecentApp : ActionData() {
950950
override val id: ActionId = ActionId.CLEAR_RECENT_APP
951951
}
952+
953+
@Serializable
954+
data class ModifySetting(
955+
val settingType: io.github.sds100.keymapper.system.settings.SettingType,
956+
val settingKey: String,
957+
val value: String,
958+
) : ActionData() {
959+
override val id: ActionId = ActionId.MODIFY_SETTING
960+
961+
override fun compareTo(other: ActionData) = when (other) {
962+
is ModifySetting -> compareValuesBy(
963+
this,
964+
other,
965+
{ it.settingType },
966+
{ it.settingKey },
967+
{ it.value },
968+
)
969+
else -> super.compareTo(other)
970+
}
971+
}
952972
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import io.github.sds100.keymapper.system.camera.CameraLens
2222
import io.github.sds100.keymapper.system.intents.IntentExtraModel
2323
import io.github.sds100.keymapper.system.intents.IntentTarget
2424
import io.github.sds100.keymapper.system.network.HttpMethod
25+
import io.github.sds100.keymapper.system.settings.SettingType
2526
import io.github.sds100.keymapper.system.volume.DndMode
2627
import io.github.sds100.keymapper.system.volume.RingerMode
2728
import io.github.sds100.keymapper.system.volume.VolumeStream
@@ -50,6 +51,7 @@ object ActionDataEntityMapper {
5051

5152
ActionEntity.Type.INTERACT_UI_ELEMENT -> ActionId.INTERACT_UI_ELEMENT
5253
ActionEntity.Type.SHELL_COMMAND -> ActionId.SHELL_COMMAND
54+
ActionEntity.Type.MODIFY_SETTING -> ActionId.MODIFY_SETTING
5355
}
5456

5557
return when (actionId) {
@@ -723,6 +725,26 @@ object ActionDataEntityMapper {
723725

724726
ActionId.FORCE_STOP_APP -> ActionData.ForceStopApp
725727
ActionId.CLEAR_RECENT_APP -> ActionData.ClearRecentApp
728+
729+
ActionId.MODIFY_SETTING -> {
730+
val value = entity.extras.getData(ActionEntity.EXTRA_SETTING_VALUE)
731+
.valueOrNull() ?: return null
732+
733+
val settingTypeString = entity.extras.getData(ActionEntity.EXTRA_SETTING_TYPE)
734+
.valueOrNull() ?: "SYSTEM" // Default to SYSTEM for backward compatibility
735+
736+
val settingType = try {
737+
SettingType.valueOf(settingTypeString)
738+
} catch (_: IllegalArgumentException) {
739+
SettingType.SYSTEM
740+
}
741+
742+
ActionData.ModifySetting(
743+
settingType = settingType,
744+
settingKey = entity.data,
745+
value = value,
746+
)
747+
}
726748
}
727749
}
728750

@@ -749,6 +771,7 @@ object ActionDataEntityMapper {
749771
is ActionData.Sound -> ActionEntity.Type.SOUND
750772
is ActionData.InteractUiElement -> ActionEntity.Type.INTERACT_UI_ELEMENT
751773
is ActionData.ShellCommand -> ActionEntity.Type.SHELL_COMMAND
774+
is ActionData.ModifySetting -> ActionEntity.Type.MODIFY_SETTING
752775
else -> ActionEntity.Type.SYSTEM_ACTION
753776
}
754777

@@ -825,6 +848,7 @@ object ActionDataEntityMapper {
825848
is ActionData.ControlMedia.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!!
826849
is ActionData.ControlMedia.Stop -> SYSTEM_ACTION_ID_MAP[data.id]!!
827850
is ActionData.GoBack -> SYSTEM_ACTION_ID_MAP[data.id]!!
851+
is ActionData.ModifySetting -> data.settingKey
828852
else -> SYSTEM_ACTION_ID_MAP[data.id]!!
829853
}
830854

@@ -1105,6 +1129,11 @@ object ActionDataEntityMapper {
11051129
EntityExtra(ActionEntity.EXTRA_SHELL_COMMAND_TIMEOUT, data.timeoutMillis.toString()),
11061130
)
11071131

1132+
is ActionData.ModifySetting -> listOf(
1133+
EntityExtra(ActionEntity.EXTRA_SETTING_VALUE, data.value),
1134+
EntityExtra(ActionEntity.EXTRA_SETTING_TYPE, data.settingType.name),
1135+
)
1136+
11081137
else -> emptyList()
11091138
}
11101139

@@ -1279,5 +1308,7 @@ object ActionDataEntityMapper {
12791308
ActionId.HTTP_REQUEST to "http_request",
12801309
ActionId.FORCE_STOP_APP to "force_stop_app",
12811310
ActionId.CLEAR_RECENT_APP to "clear_recent_app",
1311+
1312+
ActionId.MODIFY_SETTING to "modify_setting",
12821313
)
12831314
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import io.github.sds100.keymapper.system.permissions.Permission
2727
import io.github.sds100.keymapper.system.permissions.PermissionAdapter
2828
import io.github.sds100.keymapper.system.permissions.SystemFeatureAdapter
2929
import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter
30+
import io.github.sds100.keymapper.system.settings.SettingType
3031

3132
class LazyActionErrorSnapshot(
3233
private val packageManager: PackageManagerAdapter,
@@ -231,6 +232,27 @@ class LazyActionErrorSnapshot(
231232
}
232233
}
233234

235+
is ActionData.ModifySetting -> {
236+
return when (action.settingType) {
237+
SettingType.SYSTEM -> {
238+
if (!isPermissionGranted(Permission.WRITE_SETTINGS)) {
239+
SystemError.PermissionDenied(Permission.WRITE_SETTINGS)
240+
} else {
241+
null
242+
}
243+
}
244+
SettingType.SECURE,
245+
SettingType.GLOBAL,
246+
-> {
247+
if (!isPermissionGranted(Permission.WRITE_SECURE_SETTINGS)) {
248+
SystemError.PermissionDenied(Permission.WRITE_SECURE_SETTINGS)
249+
} else {
250+
null
251+
}
252+
}
253+
}
254+
}
255+
234256
else -> {}
235257
}
236258

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,6 @@ enum class ActionId {
147147

148148
FORCE_STOP_APP,
149149
CLEAR_RECENT_APP,
150+
151+
MODIFY_SETTING,
150152
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ import androidx.compose.material3.MaterialTheme
2424
import androidx.compose.material3.ModalBottomSheet
2525
import androidx.compose.material3.OutlinedButton
2626
import androidx.compose.material3.SheetState
27-
import androidx.compose.material3.SheetValue.Expanded
2827
import androidx.compose.material3.Text
2928
import androidx.compose.runtime.Composable
3029
import androidx.compose.runtime.rememberCoroutineScope
3130
import androidx.compose.ui.Alignment
3231
import androidx.compose.ui.Modifier
3332
import androidx.compose.ui.platform.LocalContext
34-
import androidx.compose.ui.platform.LocalDensity
3533
import androidx.compose.ui.platform.LocalUriHandler
3634
import androidx.compose.ui.res.stringResource
3735
import androidx.compose.ui.text.style.TextAlign
@@ -413,8 +411,8 @@ private fun Preview() {
413411
KeyMapperTheme {
414412
val sheetState = SheetState(
415413
skipPartiallyExpanded = true,
416-
density = LocalDensity.current,
417-
initialValue = Expanded,
414+
positionalThreshold = { 0f },
415+
velocityThreshold = { 0f },
418416
)
419417

420418
ActionOptionsBottomSheet(
@@ -472,8 +470,8 @@ private fun PreviewNoEditButton() {
472470
KeyMapperTheme {
473471
val sheetState = SheetState(
474472
skipPartiallyExpanded = true,
475-
density = LocalDensity.current,
476-
initialValue = Expanded,
473+
positionalThreshold = { 0f },
474+
velocityThreshold = { 0f },
477475
)
478476

479477
ActionOptionsBottomSheet(

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,13 @@ class ActionUiHelper(
651651
ActionData.Microphone.Mute -> getString(R.string.action_mute_microphone)
652652
ActionData.Microphone.Toggle -> getString(R.string.action_toggle_mute_microphone)
653653
ActionData.Microphone.Unmute -> getString(R.string.action_unmute_microphone)
654+
655+
is ActionData.ModifySetting -> {
656+
getString(
657+
R.string.modify_setting_description,
658+
arrayOf(action.settingKey, action.value),
659+
)
660+
}
654661
}
655662

656663
fun getIcon(action: ActionData): ComposeIconInfo = when (action) {

0 commit comments

Comments
 (0)