Skip to content

Commit d5949a2

Browse files
committed
#2024 feat: support Expert mode on all Android versions supported by Key Mapper (8.0+)
1 parent 994a66f commit d5949a2

File tree

30 files changed

+135
-296
lines changed

30 files changed

+135
-296
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## [4.0.4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.4)
2+
3+
#### 11 February 2026
4+
5+
## Added
6+
7+
- #2024 support Expert mode on all Android versions supported by Key Mapper (8.0+).
8+
19
## [4.0.3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.3)
210

311
#### 07 February 2026

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

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import android.content.BroadcastReceiver
55
import android.content.Context
66
import android.content.Intent
77
import android.content.IntentFilter
8-
import android.os.Build
98
import android.os.UserManager
109
import android.util.Log
1110
import android.widget.Toast
@@ -23,7 +22,6 @@ import io.github.sds100.keymapper.base.settings.Theme
2322
import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl
2423
import io.github.sds100.keymapper.base.system.notifications.NotificationController
2524
import io.github.sds100.keymapper.base.system.permissions.AutoGrantPermissionController
26-
import io.github.sds100.keymapper.common.utils.Constants
2725
import io.github.sds100.keymapper.data.Keys
2826
import io.github.sds100.keymapper.data.entities.LogEntryEntity
2927
import io.github.sds100.keymapper.data.repositories.LogRepository
@@ -231,27 +229,25 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
231229
Timber.i("KeyMapperApp: System bridge is disconnected")
232230
}
233231

234-
if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) {
235-
systemBridgeAutoStarter.init()
232+
systemBridgeAutoStarter.init()
236233

237-
// Initialize SystemBridgeLogger to start receiving log messages from SystemBridge.
238-
// Using Lazy<> to avoid circular dependency issues and ensure it's only created
239-
// when the API level requirement is met.
240-
systemBridgeLogger.start()
234+
// Initialize SystemBridgeLogger to start receiving log messages from SystemBridge.
235+
// Using Lazy<> to avoid circular dependency issues and ensure it's only created
236+
// when the API level requirement is met.
237+
systemBridgeLogger.start()
241238

242-
appCoroutineScope.launch {
243-
systemBridgeConnectionManager.connectionState.collect { state ->
244-
if (state is SystemBridgeConnectionState.Connected) {
245-
val isUsed =
246-
settingsRepository.get(Keys.isSystemBridgeUsed).first() ?: false
247-
248-
// Enable the setting to use PRO mode for key event actions the first time they use PRO mode.
249-
if (!isUsed) {
250-
settingsRepository.set(Keys.keyEventActionsUseSystemBridge, true)
251-
}
252-
253-
settingsRepository.set(Keys.isSystemBridgeUsed, true)
239+
appCoroutineScope.launch {
240+
systemBridgeConnectionManager.connectionState.collect { state ->
241+
if (state is SystemBridgeConnectionState.Connected) {
242+
val isUsed =
243+
settingsRepository.get(Keys.isSystemBridgeUsed).first() ?: false
244+
245+
// Enable the setting to use PRO mode for key event actions the first time they use PRO mode.
246+
if (!isUsed) {
247+
settingsRepository.set(Keys.keyEventActionsUseSystemBridge, true)
254248
}
249+
250+
settingsRepository.set(Keys.isSystemBridgeUsed, true)
255251
}
256252
}
257253
}

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

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper
66
import io.github.sds100.keymapper.base.system.inputmethod.SwitchImeInterface
77
import io.github.sds100.keymapper.common.BuildConfigProvider
88
import io.github.sds100.keymapper.common.models.ShellExecutionMode
9-
import io.github.sds100.keymapper.common.utils.Constants
109
import io.github.sds100.keymapper.common.utils.KMError
1110
import io.github.sds100.keymapper.common.utils.firstBlocking
1211
import io.github.sds100.keymapper.common.utils.onFailure
@@ -67,19 +66,11 @@ class LazyActionErrorSnapshot(
6766
}
6867

6968
private val isSystemBridgeConnected: Boolean by lazy {
70-
if (buildConfigProvider.sdkInt >= Constants.SYSTEM_BRIDGE_MIN_API) {
71-
systemBridgeConnectionManager.isConnected()
72-
} else {
73-
false
74-
}
69+
systemBridgeConnectionManager.isConnected()
7570
}
7671

7772
private val keyEventActionsUseSystemBridge: Boolean by lazy {
78-
if (buildConfigProvider.sdkInt >= Constants.SYSTEM_BRIDGE_MIN_API) {
79-
preferenceRepository.get(Keys.keyEventActionsUseSystemBridge).firstBlocking() ?: false
80-
} else {
81-
false
82-
}
73+
preferenceRepository.get(Keys.keyEventActionsUseSystemBridge).firstBlocking() ?: false
8374
}
8475

8576
override fun getErrors(actions: List<ActionData>): Map<ActionData, KMError?> {
@@ -129,8 +120,7 @@ class LazyActionErrorSnapshot(
129120
return isSupportedError
130121
}
131122

132-
if (buildConfigProvider.sdkInt >= Constants.SYSTEM_BRIDGE_MIN_API &&
133-
action is ActionData.InputKeyEvent &&
123+
if (action is ActionData.InputKeyEvent &&
134124
keyEventActionsUseSystemBridge
135125
) {
136126
if (!isSystemBridgeConnected) {
@@ -155,8 +145,7 @@ class LazyActionErrorSnapshot(
155145
}
156146

157147
@SuppressLint("NewApi")
158-
if (buildConfigProvider.sdkInt >= Constants.SYSTEM_BRIDGE_MIN_API &&
159-
ActionUtils.isSystemBridgeRequired(action.id) &&
148+
if (ActionUtils.isSystemBridgeRequired(action.id) &&
160149
!isSystemBridgeConnected
161150
) {
162151
return SystemBridgeError.Disconnected
@@ -241,6 +230,7 @@ class LazyActionErrorSnapshot(
241230
null
242231
}
243232
}
233+
244234
SettingType.SECURE,
245235
SettingType.GLOBAL,
246236
-> {

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package io.github.sds100.keymapper.base.actions
33
import android.content.pm.PackageManager
44
import android.os.Build
55
import androidx.annotation.DrawableRes
6-
import androidx.annotation.RequiresApi
76
import androidx.annotation.StringRes
87
import androidx.compose.material.icons.Icons
98
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
@@ -96,8 +95,6 @@ import io.github.sds100.keymapper.system.permissions.Permission
9695

9796
object ActionUtils {
9897

99-
val isSystemBridgeSupported = Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API
100-
10198
@StringRes
10299
fun getCategoryLabel(category: ActionCategory): Int = when (category) {
103100
ActionCategory.NAVIGATION -> R.string.action_cat_navigation
@@ -750,42 +747,44 @@ object ActionUtils {
750747
else -> emptyList()
751748
}
752749

753-
@RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API)
754750
fun isSystemBridgeRequired(id: ActionId): Boolean {
751+
// Actions are only tested on Android 10 and higher.
755752
return when (id) {
756753
ActionId.ENABLE_WIFI,
757754
ActionId.DISABLE_WIFI,
758755
ActionId.TOGGLE_WIFI,
759-
-> true
756+
-> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
760757

761758
ActionId.TOGGLE_MOBILE_DATA,
762759
ActionId.ENABLE_MOBILE_DATA,
763760
ActionId.DISABLE_MOBILE_DATA,
764-
-> true
761+
-> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
765762

766763
ActionId.TOGGLE_HOTSPOT,
767764
ActionId.ENABLE_HOTSPOT,
768765
ActionId.DISABLE_HOTSPOT,
769-
-> true
766+
-> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
770767

771768
ActionId.ENABLE_NFC,
772769
ActionId.DISABLE_NFC,
773770
ActionId.TOGGLE_NFC,
774-
-> true
771+
-> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
775772

776773
ActionId.TOGGLE_AIRPLANE_MODE,
777774
ActionId.ENABLE_AIRPLANE_MODE,
778775
ActionId.DISABLE_AIRPLANE_MODE,
779-
-> true
776+
-> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
780777

781778
ActionId.TOGGLE_BLUETOOTH,
782779
ActionId.ENABLE_BLUETOOTH,
783780
ActionId.DISABLE_BLUETOOTH,
784781
-> Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2
785782

786-
ActionId.POWER_ON_OFF_DEVICE -> true
783+
ActionId.POWER_ON_OFF_DEVICE -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
787784

788-
ActionId.FORCE_STOP_APP, ActionId.CLEAR_RECENT_APP -> true
785+
ActionId.FORCE_STOP_APP, ActionId.CLEAR_RECENT_APP ->
786+
Build.VERSION.SDK_INT >=
787+
Build.VERSION_CODES.Q
789788

790789
else -> false
791790
}
@@ -796,7 +795,7 @@ object ActionUtils {
796795
ActionId.TOGGLE_MOBILE_DATA,
797796
ActionId.ENABLE_MOBILE_DATA,
798797
ActionId.DISABLE_MOBILE_DATA,
799-
-> return if (isSystemBridgeSupported) {
798+
-> return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
800799
emptyList()
801800
} else {
802801
listOf(Permission.ROOT)
@@ -869,7 +868,7 @@ object ActionUtils {
869868
ActionId.ENABLE_NFC,
870869
ActionId.DISABLE_NFC,
871870
ActionId.TOGGLE_NFC,
872-
-> return if (isSystemBridgeSupported) {
871+
-> return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
873872
emptyList()
874873
} else {
875874
listOf(Permission.ROOT)
@@ -887,7 +886,7 @@ object ActionUtils {
887886
ActionId.TOGGLE_AIRPLANE_MODE,
888887
ActionId.ENABLE_AIRPLANE_MODE,
889888
ActionId.DISABLE_AIRPLANE_MODE,
890-
-> return if (isSystemBridgeSupported) {
889+
-> return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
891890
emptyList()
892891
} else {
893892
listOf(Permission.ROOT)
@@ -903,11 +902,12 @@ object ActionUtils {
903902

904903
ActionId.SECURE_LOCK_DEVICE -> return listOf(Permission.DEVICE_ADMIN)
905904

906-
ActionId.POWER_ON_OFF_DEVICE -> return if (isSystemBridgeSupported) {
907-
emptyList()
908-
} else {
909-
listOf(Permission.ROOT)
910-
}
905+
ActionId.POWER_ON_OFF_DEVICE ->
906+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
907+
emptyList()
908+
} else {
909+
listOf(Permission.ROOT)
910+
}
911911

912912
ActionId.DISMISS_ALL_NOTIFICATIONS,
913913
ActionId.DISMISS_MOST_RECENT_NOTIFICATION,

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

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.github.sds100.keymapper.base.actions
22

3-
import android.os.Build
43
import androidx.compose.runtime.getValue
54
import androidx.compose.runtime.mutableStateOf
65
import androidx.compose.runtime.setValue
@@ -15,7 +14,6 @@ import io.github.sds100.keymapper.base.utils.navigation.navigate
1514
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
1615
import io.github.sds100.keymapper.common.models.ShellExecutionMode
1716
import io.github.sds100.keymapper.common.models.isExecuting
18-
import io.github.sds100.keymapper.common.utils.Constants
1917
import io.github.sds100.keymapper.common.utils.handle
2018
import io.github.sds100.keymapper.data.Keys
2119
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
@@ -45,16 +43,14 @@ class ConfigShellCommandViewModel @Inject constructor(
4543

4644
init {
4745
// Update ExpertModeStatus in state
48-
if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) {
49-
viewModelScope.launch {
50-
systemBridgeConnectionManager.connectionState.map { connectionState ->
51-
when (connectionState) {
52-
is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED
53-
is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED
54-
}
55-
}.collect { expertModeStatus ->
56-
state = state.copy(expertModeStatus = expertModeStatus)
46+
viewModelScope.launch {
47+
systemBridgeConnectionManager.connectionState.map { connectionState ->
48+
when (connectionState) {
49+
is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED
50+
is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED
5751
}
52+
}.collect { expertModeStatus ->
53+
state = state.copy(expertModeStatus = expertModeStatus)
5854
}
5955
}
6056

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.github.sds100.keymapper.base.actions
22

3-
import android.os.Build
43
import io.github.sds100.keymapper.common.models.ShellExecutionMode
54
import io.github.sds100.keymapper.common.models.ShellResult
65
import io.github.sds100.keymapper.common.utils.KMError
@@ -58,19 +57,15 @@ class ExecuteShellCommandUseCase @Inject constructor(
5857
command: String,
5958
timeoutMillis: Long,
6059
): KMResult<ShellResult> {
61-
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
62-
runInterruptible(Dispatchers.IO) {
63-
try {
64-
systemBridgeConnectionManager.run { systemBridge ->
65-
systemBridge.executeCommand(command, timeoutMillis)
66-
}
67-
// Only some standard exceptions can be thrown across Binder.
68-
} catch (e: IllegalStateException) {
69-
KMError.ShellCommandTimeout(timeoutMillis, null)
60+
return runInterruptible(Dispatchers.IO) {
61+
try {
62+
systemBridgeConnectionManager.run { systemBridge ->
63+
systemBridge.executeCommand(command, timeoutMillis)
7064
}
65+
// Only some standard exceptions can be thrown across Binder.
66+
} catch (e: IllegalStateException) {
67+
KMError.ShellCommandTimeout(timeoutMillis, null)
7168
}
72-
} else {
73-
KMError.SdkVersionTooLow(Build.VERSION_CODES.Q)
7469
}
7570
}
7671
}

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package io.github.sds100.keymapper.base.actions
22

3-
import android.os.Build
43
import io.github.sds100.keymapper.base.actions.sound.SoundsManager
54
import io.github.sds100.keymapper.base.system.inputmethod.SwitchImeInterface
65
import io.github.sds100.keymapper.common.BuildConfigProvider
7-
import io.github.sds100.keymapper.common.utils.Constants
86
import io.github.sds100.keymapper.data.Keys
97
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
108
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
@@ -20,7 +18,6 @@ import kotlinx.coroutines.flow.Flow
2018
import kotlinx.coroutines.flow.channelFlow
2119
import kotlinx.coroutines.flow.collectLatest
2220
import kotlinx.coroutines.flow.drop
23-
import kotlinx.coroutines.flow.emptyFlow
2421
import kotlinx.coroutines.flow.map
2522
import kotlinx.coroutines.flow.merge
2623

@@ -46,14 +43,10 @@ class GetActionErrorUseCaseImpl @Inject constructor(
4643
permissionAdapter.onPermissionsUpdate,
4744
soundsManager.soundFiles.drop(1).map { },
4845
packageManagerAdapter.onPackagesChanged,
49-
if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) {
50-
merge(
51-
systemBridgeConnectionManager.connectionState.drop(1).map { },
52-
preferenceRepository.get(Keys.keyEventActionsUseSystemBridge),
53-
)
54-
} else {
55-
emptyFlow()
56-
},
46+
merge(
47+
systemBridgeConnectionManager.connectionState.drop(1).map { },
48+
preferenceRepository.get(Keys.keyEventActionsUseSystemBridge),
49+
),
5750
)
5851

5952
override val actionErrorSnapshot: Flow<ActionErrorSnapshot> = channelFlow {

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import io.github.sds100.keymapper.base.system.navigation.OpenMenuHelper
2121
import io.github.sds100.keymapper.base.system.notifications.NotificationController
2222
import io.github.sds100.keymapper.base.utils.getFullMessage
2323
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
24-
import io.github.sds100.keymapper.common.utils.Constants
2524
import io.github.sds100.keymapper.common.utils.InputEventAction
2625
import io.github.sds100.keymapper.common.utils.KMError
2726
import io.github.sds100.keymapper.common.utils.KMError.SdkVersionTooLow
@@ -879,9 +878,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor(
879878
}
880879

881880
is ActionData.ScreenOnOff -> {
882-
if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API &&
883-
systemBridgeConnectionManager.isConnected()
884-
) {
881+
if (systemBridgeConnectionManager.isConnected()) {
885882
val model = InjectKeyEventModel(
886883
keyCode = KeyEvent.KEYCODE_POWER,
887884
action = KeyEvent.ACTION_DOWN,
@@ -1023,12 +1020,10 @@ class PerformActionsUseCaseImpl @AssistedInject constructor(
10231020

10241021
if (packageName == null) {
10251022
result = KMError.Exception(Exception("No foreground app found to kill"))
1026-
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
1023+
} else {
10271024
result = systemBridgeConnectionManager.run { systemBridge ->
10281025
systemBridge.forceStopPackage(packageName)
10291026
}
1030-
} else {
1031-
result = SdkVersionTooLow(minSdk = Constants.SYSTEM_BRIDGE_MIN_API)
10321027
}
10331028
}
10341029

@@ -1046,7 +1041,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor(
10461041
systemBridge.removeTasks(packageName)
10471042
}
10481043
} else {
1049-
result = SdkVersionTooLow(minSdk = Constants.SYSTEM_BRIDGE_MIN_API)
1044+
result = SdkVersionTooLow(minSdk = Build.VERSION_CODES.Q)
10501045
}
10511046
}
10521047

0 commit comments

Comments
 (0)