Skip to content

Commit 53bde3f

Browse files
committed
Merge branch 'develop' into copilot/add-toggle-hotspot-action
2 parents e29329c + 9df0565 commit 53bde3f

80 files changed

Lines changed: 2515 additions & 344 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
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+
- #1221 action to show a custom notification
8+
9+
## [4.0.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.02)
10+
11+
#### 08 November 2025
12+
13+
## Added
14+
15+
- #1890 add button to save log to file and share it. The clipboard button now cuts off older entries and keeps newest ones.
16+
17+
## Fixed
18+
19+
- Only autostart PRO mode with Shizuku if Shizuku permission is granted. Otherwise fallback to method with Wireless Debugging and WRITE_SECURE_SETTINGS permission.
20+
- Starting system bridge for the first time would be janky because granting READ_LOGS kills the app process. Only grant for READ_LOGS when sharing logcat from settings.
21+
- #1886 mobile data actions work in PRO mode.
22+
123
## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01)
224

325
#### 01 November 2025

app/proguard-rules.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
-keep class io.github.sds100.keymapper.api.IKeyEventRelayService$Stub { *; }
7676
-keep class io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback { *; }
7777
-keep class io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback$Stub { *; }
78+
-keep class com.android.internal.telephony.ITelephony { *; }
79+
-keep class com.android.internal.telephony.ITelephony$Stub { *; }
7880

7981
-keepattributes *Annotation*, InnerClasses
8082
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations

app/version.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
VERSION_NAME=4.0.0-beta.2
2-
VERSION_CODE=186
1+
VERSION_NAME=4.0.0-beta.3
2+
VERSION_CODE=189
33
VERSION_NUM=01

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
✨ Screen-off remapping
2-
You can now remap buttons when the screen is off (including the power button) for free with PRO mode.
2+
You can now remap ALL buttons when the screen is off (including the power button) for free with PRO mode.
33

44
🎯 New Actions
55
• Run shell commands
66
• Send SMS messages
77
• Force stop current app or clear from recents
88
• Mute/unmute microphone
9+
• Modify any system setting
10+
• Show a custom notification
911

1012
🆕 New Features
1113
• Redesigned Settings screen

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import io.github.sds100.keymapper.data.repositories.LogRepository
2929
import io.github.sds100.keymapper.data.repositories.PreferenceRepositoryImpl
3030
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl
3131
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
32+
import io.github.sds100.keymapper.sysbridge.manager.isConnected
3233
import io.github.sds100.keymapper.system.apps.AndroidPackageManagerAdapter
3334
import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter
3435
import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl
@@ -224,6 +225,12 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
224225
autoGrantPermissionController.start()
225226
keyEventRelayServiceWrapper.bind()
226227

228+
if (systemBridgeConnectionManager.isConnected()) {
229+
Timber.i("KeyMapperApp: System bridge is connected")
230+
} else {
231+
Timber.i("KeyMapperApp: System bridge is disconnected")
232+
}
233+
227234
if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) {
228235
systemBridgeAutoStarter.init()
229236

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

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@ 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
2827
import io.github.sds100.keymapper.base.compose.ComposeColors
2928
import io.github.sds100.keymapper.base.input.InputEventDetectionSource
3029
import io.github.sds100.keymapper.base.input.InputEventHubImpl
30+
import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapStateImpl
3131
import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase
3232
import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl
3333
import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate
34-
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
@@ -56,9 +56,6 @@ abstract class BaseMainActivity : AppCompatActivity() {
5656
const val ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG =
5757
"${BuildConfig.LIBRARY_PACKAGE_NAME}.ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG"
5858

59-
const val ACTION_USE_FLOATING_BUTTONS =
60-
"${BuildConfig.LIBRARY_PACKAGE_NAME}.ACTION_USE_FLOATING_BUTTONS"
61-
6259
const val ACTION_SAVE_FILE = "${BuildConfig.LIBRARY_PACKAGE_NAME}.ACTION_SAVE_FILE"
6360
const val EXTRA_FILE_URI = "${BuildConfig.LIBRARY_PACKAGE_NAME}.EXTRA_FILE_URI"
6461

@@ -78,9 +75,6 @@ abstract class BaseMainActivity : AppCompatActivity() {
7875
@Inject
7976
lateinit var onboardingUseCase: OnboardingUseCase
8077

81-
@Inject
82-
lateinit var recordTriggerController: RecordTriggerControllerImpl
83-
8478
@Inject
8579
lateinit var notificationReceiverAdapter: NotificationReceiverAdapterImpl
8680

@@ -105,6 +99,12 @@ abstract class BaseMainActivity : AppCompatActivity() {
10599
@Inject
106100
lateinit var inputEventHub: InputEventHubImpl
107101

102+
@Inject
103+
lateinit var navigationProvider: NavigationProvider
104+
105+
@Inject
106+
lateinit var configKeyMapState: ConfigKeyMapStateImpl
107+
108108
private lateinit var requestPermissionDelegate: RequestPermissionDelegate
109109

110110
private val currentNightMode: Int
@@ -155,22 +155,23 @@ abstract class BaseMainActivity : AppCompatActivity() {
155155
)
156156
super.onCreate(savedInstanceState)
157157

158+
savedInstanceState?.let { configKeyMapState.restoreState(it) }
159+
158160
requestPermissionDelegate = RequestPermissionDelegate(
159161
this,
160162
showDialogs = true,
161163
permissionAdapter,
162164
notificationReceiverAdapter = notificationReceiverAdapter,
163165
buildConfigProvider = buildConfigProvider,
164166
shizukuAdapter = shizukuAdapter,
167+
navigationProvider = navigationProvider,
168+
coroutineScope = lifecycleScope,
165169
)
166170

167171
permissionAdapter.request
168172
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
169173
.onEach { permission ->
170-
requestPermissionDelegate.requestPermission(
171-
permission,
172-
findNavController(R.id.container),
173-
)
174+
requestPermissionDelegate.requestPermission(permission)
174175
}
175176
.launchIn(lifecycleScope)
176177

@@ -201,9 +202,16 @@ abstract class BaseMainActivity : AppCompatActivity() {
201202
// the activities have not necessarily resumed at that point.
202203
permissionAdapter.onPermissionsChanged()
203204
serviceAdapter.invalidateState()
204-
suAdapter.invalidateIsRooted()
205+
suAdapter.requestPermission()
205206
systemBridgeSetupController.invalidateSettings()
206207
networkAdapter.invalidateState()
208+
onboardingUseCase.handledMigrateScreenOffKeyMapsNotification()
209+
}
210+
211+
override fun onSaveInstanceState(outState: Bundle) {
212+
configKeyMapState.saveState(outState)
213+
214+
super.onSaveInstanceState(outState)
207215
}
208216

209217
override fun onDestroy() {

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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,17 @@ sealed class ActionData : Comparable<ActionData> {
889889
override val id: ActionId = ActionId.DISMISS_ALL_NOTIFICATIONS
890890
}
891891

892+
@Serializable
893+
data class CreateNotification(val title: String, val text: String, val timeoutMs: Long?) :
894+
ActionData() {
895+
override val id: ActionId = ActionId.CREATE_NOTIFICATION
896+
897+
override fun compareTo(other: ActionData) = when (other) {
898+
is CreateNotification -> title.compareTo(other.title)
899+
else -> super.compareTo(other)
900+
}
901+
}
902+
892903
@Serializable
893904
data object AnswerCall : ActionData() {
894905
override val id: ActionId = ActionId.ANSWER_PHONE_CALL
@@ -967,4 +978,24 @@ sealed class ActionData : Comparable<ActionData> {
967978
data object ClearRecentApp : ActionData() {
968979
override val id: ActionId = ActionId.CLEAR_RECENT_APP
969980
}
981+
982+
@Serializable
983+
data class ModifySetting(
984+
val settingType: io.github.sds100.keymapper.system.settings.SettingType,
985+
val settingKey: String,
986+
val value: String,
987+
) : ActionData() {
988+
override val id: ActionId = ActionId.MODIFY_SETTING
989+
990+
override fun compareTo(other: ActionData) = when (other) {
991+
is ModifySetting -> compareValuesBy(
992+
this,
993+
other,
994+
{ it.settingType },
995+
{ it.settingKey },
996+
{ it.value },
997+
)
998+
else -> super.compareTo(other)
999+
}
1000+
}
9701001
}

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

Lines changed: 60 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,8 @@ 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
55+
ActionEntity.Type.CREATE_NOTIFICATION -> ActionId.CREATE_NOTIFICATION
5356
}
5457

5558
return when (actionId) {
@@ -560,6 +563,25 @@ object ActionDataEntityMapper {
560563
ActionId.SHOW_POWER_MENU -> ActionData.ShowPowerMenu
561564
ActionId.DISMISS_MOST_RECENT_NOTIFICATION -> ActionData.DismissLastNotification
562565
ActionId.DISMISS_ALL_NOTIFICATIONS -> ActionData.DismissAllNotifications
566+
ActionId.CREATE_NOTIFICATION -> {
567+
val title =
568+
entity.extras.getData(ActionEntity.EXTRA_NOTIFICATION_TITLE).valueOrNull()
569+
?: return null
570+
571+
val text = entity.data.takeIf { it.isNotBlank() }
572+
?: return null
573+
574+
val timeoutMs = entity.extras.getData(
575+
ActionEntity.EXTRA_NOTIFICATION_TIMEOUT,
576+
).valueOrNull()
577+
?.toLongOrNull()
578+
579+
ActionData.CreateNotification(
580+
title = title,
581+
text = text,
582+
timeoutMs = timeoutMs,
583+
)
584+
}
563585
ActionId.ANSWER_PHONE_CALL -> ActionData.AnswerCall
564586
ActionId.END_PHONE_CALL -> ActionData.EndCall
565587
ActionId.DEVICE_CONTROLS -> ActionData.DeviceControls
@@ -727,6 +749,26 @@ object ActionDataEntityMapper {
727749

728750
ActionId.FORCE_STOP_APP -> ActionData.ForceStopApp
729751
ActionId.CLEAR_RECENT_APP -> ActionData.ClearRecentApp
752+
753+
ActionId.MODIFY_SETTING -> {
754+
val value = entity.extras.getData(ActionEntity.EXTRA_SETTING_VALUE)
755+
.valueOrNull() ?: return null
756+
757+
val settingTypeString = entity.extras.getData(ActionEntity.EXTRA_SETTING_TYPE)
758+
.valueOrNull() ?: "SYSTEM" // Default to SYSTEM for backward compatibility
759+
760+
val settingType = try {
761+
SettingType.valueOf(settingTypeString)
762+
} catch (_: IllegalArgumentException) {
763+
SettingType.SYSTEM
764+
}
765+
766+
ActionData.ModifySetting(
767+
settingType = settingType,
768+
settingKey = entity.data,
769+
value = value,
770+
)
771+
}
730772
}
731773
}
732774

@@ -753,6 +795,8 @@ object ActionDataEntityMapper {
753795
is ActionData.Sound -> ActionEntity.Type.SOUND
754796
is ActionData.InteractUiElement -> ActionEntity.Type.INTERACT_UI_ELEMENT
755797
is ActionData.ShellCommand -> ActionEntity.Type.SHELL_COMMAND
798+
is ActionData.ModifySetting -> ActionEntity.Type.MODIFY_SETTING
799+
is ActionData.CreateNotification -> ActionEntity.Type.CREATE_NOTIFICATION
756800
else -> ActionEntity.Type.SYSTEM_ACTION
757801
}
758802

@@ -823,12 +867,14 @@ object ActionDataEntityMapper {
823867
data.command.toByteArray(),
824868
Base64.DEFAULT,
825869
).trim() // Trim to remove trailing newline added by Base64.DEFAULT
870+
is ActionData.CreateNotification -> data.text
826871
is ActionData.HttpRequest -> SYSTEM_ACTION_ID_MAP[data.id]!!
827872
is ActionData.ControlMediaForApp.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!!
828873
is ActionData.ControlMediaForApp.Stop -> SYSTEM_ACTION_ID_MAP[data.id]!!
829874
is ActionData.ControlMedia.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!!
830875
is ActionData.ControlMedia.Stop -> SYSTEM_ACTION_ID_MAP[data.id]!!
831876
is ActionData.GoBack -> SYSTEM_ACTION_ID_MAP[data.id]!!
877+
is ActionData.ModifySetting -> data.settingKey
832878
else -> SYSTEM_ACTION_ID_MAP[data.id]!!
833879
}
834880

@@ -1109,6 +1155,18 @@ object ActionDataEntityMapper {
11091155
EntityExtra(ActionEntity.EXTRA_SHELL_COMMAND_TIMEOUT, data.timeoutMillis.toString()),
11101156
)
11111157

1158+
is ActionData.ModifySetting -> listOf(
1159+
EntityExtra(ActionEntity.EXTRA_SETTING_VALUE, data.value),
1160+
EntityExtra(ActionEntity.EXTRA_SETTING_TYPE, data.settingType.name),
1161+
)
1162+
1163+
is ActionData.CreateNotification -> buildList {
1164+
add(EntityExtra(ActionEntity.EXTRA_NOTIFICATION_TITLE, data.title))
1165+
data.timeoutMs?.let {
1166+
add(EntityExtra(ActionEntity.EXTRA_NOTIFICATION_TIMEOUT, it.toString()))
1167+
}
1168+
}
1169+
11121170
else -> emptyList()
11131171
}
11141172

@@ -1287,5 +1345,7 @@ object ActionDataEntityMapper {
12871345
ActionId.HTTP_REQUEST to "http_request",
12881346
ActionId.FORCE_STOP_APP to "force_stop_app",
12891347
ActionId.CLEAR_RECENT_APP to "clear_recent_app",
1348+
1349+
ActionId.MODIFY_SETTING to "modify_setting",
12901350
)
12911351
}

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

0 commit comments

Comments
 (0)