Skip to content

Commit 6d795b7

Browse files
authored
Merge pull request #2046 from keymapperorg/develop
Version 4.0.4
2 parents bc3f8ae + c4a11b0 commit 6d795b7

File tree

51 files changed

+696
-496
lines changed

Some content is hidden

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

51 files changed

+696
-496
lines changed

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
blank_issues_enabled: false
22
contact_links:
33
- name: Need help using the app?
4-
url: http://keymapper.club?utm_source=issue_template_chooser
4+
url: http://keymapper.app/discord?utm_source=issue_template_chooser
55
about: Join the Discord server and let us help you

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
## [4.0.4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.4)
2+
3+
#### 21 February 2026
4+
5+
## Added
6+
7+
- #2024 support Expert mode on all Android versions supported by Key Mapper (8.0+).
8+
- #2025 add report bug button to home screen menu.
9+
- #2027 Make the key map sorting feature easier to understand.
10+
- #2016 Show a warning when repeating a key code action less than 20 ms with expert mode triggers.
11+
- Show dialog if Expert mode fails to start after 60 seconds instead of waiting indefinitely.
12+
13+
## Fixed
14+
15+
- #2030 do not filter out unknown evdev key events.
16+
- #2028 work around Shizuku bug on Mediatek devices that prevents Expert mode from starting.
17+
- #2034 catch errors when injecting events with Expert mode on Xiaomi devices and show warning to fix on home screen.
18+
119
## [4.0.3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.3)
220

321
#### 07 February 2026

app/version.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
VERSION_NAME=4.0.3
2-
VERSION_CODE=243
1+
VERSION_NAME=4.0.4
2+
VERSION_CODE=246

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/ActionOptionsBottomSheet.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.foundation.rememberScrollState
1414
import androidx.compose.foundation.verticalScroll
1515
import androidx.compose.material.icons.Icons
1616
import androidx.compose.material.icons.automirrored.rounded.HelpOutline
17+
import androidx.compose.material.icons.rounded.Warning
1718
import androidx.compose.material3.ButtonDefaults
1819
import androidx.compose.material3.ExperimentalMaterial3Api
1920
import androidx.compose.material3.FilledTonalButton
@@ -126,6 +127,29 @@ fun ActionOptionsBottomSheet(
126127
)
127128
}
128129

130+
if (state.showRepeatRateWarning) {
131+
Spacer(Modifier.height(8.dp))
132+
133+
Row(
134+
verticalAlignment = Alignment.CenterVertically,
135+
) {
136+
Spacer(Modifier.width(16.dp))
137+
Icon(
138+
Icons.Rounded.Warning,
139+
contentDescription = null,
140+
tint = MaterialTheme.colorScheme.error,
141+
)
142+
Spacer(Modifier.width(8.dp))
143+
Text(
144+
modifier = Modifier.weight(1f),
145+
text = stringResource(R.string.action_repeat_rate_warning),
146+
color = MaterialTheme.colorScheme.error,
147+
style = MaterialTheme.typography.labelLarge,
148+
)
149+
Spacer(Modifier.width(16.dp))
150+
}
151+
}
152+
129153
if (state.showRepeatRate) {
130154
Spacer(Modifier.height(8.dp))
131155

@@ -421,6 +445,7 @@ private fun Preview() {
421445
showEditButton = true,
422446
showRepeat = true,
423447
isRepeatChecked = true,
448+
showRepeatRateWarning = true,
424449

425450
showRepeatRate = true,
426451
repeatRate = 400,
@@ -480,6 +505,7 @@ private fun PreviewNoEditButton() {
480505
showEditButton = false,
481506
showRepeat = true,
482507
isRepeatChecked = true,
508+
showRepeatRateWarning = true,
483509

484510
showRepeatRate = true,
485511
repeatRate = 400,

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/ConfigActionsViewModel.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget
1111
import io.github.sds100.keymapper.base.onboarding.OnboardingTipDelegate
1212
import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase
1313
import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegate
14+
import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey
1415
import io.github.sds100.keymapper.base.utils.getFullMessage
1516
import io.github.sds100.keymapper.base.utils.isFixable
1617
import io.github.sds100.keymapper.base.utils.navigation.NavDestination
@@ -261,6 +262,7 @@ class ConfigActionsViewModel @Inject constructor(
261262
)
262263

263264
RepeatMode.LIMIT_REACHED -> config.setActionStopRepeatingWhenLimitReached(uid)
265+
264266
RepeatMode.TRIGGER_PRESSED_AGAIN ->
265267
config.setActionStopRepeatingWhenTriggerPressedAgain(uid)
266268
}
@@ -395,10 +397,17 @@ class ConfigActionsViewModel @Inject constructor(
395397
Int.MAX_VALUE
396398
}
397399

400+
val showRepeatRateWarning =
401+
keyMap.isRepeatingActionsAllowed() &&
402+
action.data is ActionData.InputKeyEvent &&
403+
(action.repeatRate ?: defaultRepeatRate) < 20 &&
404+
keyMap.trigger.keys.any { it is EvdevTriggerKey }
405+
398406
return ActionOptionsState(
399407
showEditButton = action.data.isEditable(),
400408

401409
showRepeat = keyMap.isRepeatingActionsAllowed(),
410+
showRepeatRateWarning = showRepeatRateWarning,
402411
isRepeatChecked = action.repeat,
403412

404413
showRepeatRate = keyMap.isChangingActionRepeatRateAllowed(action),
@@ -470,6 +479,7 @@ data class ActionOptionsState(
470479
val isRepeatChecked: Boolean,
471480

472481
val showRepeatRate: Boolean,
482+
val showRepeatRateWarning: Boolean,
473483
val repeatRate: Int,
474484
val defaultRepeatRate: Int,
475485

0 commit comments

Comments
 (0)