From 994a66fedc74ea56c31a18d011452f571d13d699 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 11 Feb 2026 15:29:59 +0100 Subject: [PATCH 01/17] chore: bump version code to 4.0.4 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index 584436fb6f..bb6898e9f9 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.0.3 -VERSION_CODE=243 +VERSION_NAME=4.0.4 +VERSION_CODE=246 From d5949a2cd3e4ea3d727ab0373c270b104e670f6c Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 11 Feb 2026 15:30:57 +0100 Subject: [PATCH 02/17] #2024 feat: support Expert mode on all Android versions supported by Key Mapper (8.0+) --- CHANGELOG.md | 8 ++++ .../sds100/keymapper/base/BaseKeyMapperApp.kt | 36 +++++++------- .../base/actions/ActionErrorSnapshot.kt | 20 ++------ .../keymapper/base/actions/ActionUtils.kt | 38 +++++++-------- .../actions/ConfigShellCommandViewModel.kt | 18 +++---- .../actions/ExecuteShellCommandUseCase.kt | 19 +++----- .../base/actions/GetActionErrorUseCase.kt | 15 ++---- .../base/actions/PerformActionsUseCase.kt | 11 ++--- .../keyevent/FixKeyEventActionDelegate.kt | 13 ++--- .../expertmode/SystemBridgeAutoStarter.kt | 3 -- .../SystemBridgeSetupAssistantController.kt | 2 - .../expertmode/SystemBridgeSetupUseCase.kt | 2 - .../base/input/EvdevDevicesDelegate.kt | 3 -- .../keymapper/base/input/InputEventHub.kt | 48 +++++-------------- .../base/keymaps/DisplayKeyMapUseCase.kt | 17 ++----- .../base/logging/SystemBridgeLogger.kt | 3 -- .../base/onboarding/OnboardingTipDelegate.kt | 5 +- .../keymapper/base/settings/SettingsScreen.kt | 27 +---------- .../BaseAccessibilityServiceController.kt | 21 ++------ .../notifications/NotificationController.kt | 16 +++---- .../trigger/BaseConfigTriggerViewModel.kt | 29 ++++------- .../base/trigger/TriggerSetupDelegate.kt | 33 ++++--------- .../keymapper/common/utils/Constants.kt | 1 - .../manager/SystemBridgeConnectionManager.kt | 8 ---- .../service/SystemBridgeSetupController.kt | 4 -- .../AndroidAirplaneModeAdapter.kt | 3 +- .../system/network/AndroidNetworkAdapter.kt | 9 ++-- .../keymapper/system/nfc/AndroidNfcAdapter.kt | 5 +- .../permissions/AndroidPermissionAdapter.kt | 7 +-- .../system/volume/AndroidVolumeAdapter.kt | 7 +-- 30 files changed, 135 insertions(+), 296 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45a9593b00..b5ada825bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [4.0.4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.4) + +#### 11 February 2026 + +## Added + +- #2024 support Expert mode on all Android versions supported by Key Mapper (8.0+). + ## [4.0.3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.3) #### 07 February 2026 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index e989b9748c..ee9e54e816 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -5,7 +5,6 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.os.Build import android.os.UserManager import android.util.Log import android.widget.Toast @@ -23,7 +22,6 @@ import io.github.sds100.keymapper.base.settings.Theme import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.system.permissions.AutoGrantPermissionController -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.data.repositories.LogRepository @@ -231,27 +229,25 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { Timber.i("KeyMapperApp: System bridge is disconnected") } - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - systemBridgeAutoStarter.init() + systemBridgeAutoStarter.init() - // Initialize SystemBridgeLogger to start receiving log messages from SystemBridge. - // Using Lazy<> to avoid circular dependency issues and ensure it's only created - // when the API level requirement is met. - systemBridgeLogger.start() + // Initialize SystemBridgeLogger to start receiving log messages from SystemBridge. + // Using Lazy<> to avoid circular dependency issues and ensure it's only created + // when the API level requirement is met. + systemBridgeLogger.start() - appCoroutineScope.launch { - systemBridgeConnectionManager.connectionState.collect { state -> - if (state is SystemBridgeConnectionState.Connected) { - val isUsed = - settingsRepository.get(Keys.isSystemBridgeUsed).first() ?: false - - // Enable the setting to use PRO mode for key event actions the first time they use PRO mode. - if (!isUsed) { - settingsRepository.set(Keys.keyEventActionsUseSystemBridge, true) - } - - settingsRepository.set(Keys.isSystemBridgeUsed, true) + appCoroutineScope.launch { + systemBridgeConnectionManager.connectionState.collect { state -> + if (state is SystemBridgeConnectionState.Connected) { + val isUsed = + settingsRepository.get(Keys.isSystemBridgeUsed).first() ?: false + + // Enable the setting to use PRO mode for key event actions the first time they use PRO mode. + if (!isUsed) { + settingsRepository.set(Keys.keyEventActionsUseSystemBridge, true) } + + settingsRepository.set(Keys.isSystemBridgeUsed, true) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt index 45d67f71cf..55b487ceba 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt @@ -6,7 +6,6 @@ import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.base.system.inputmethod.SwitchImeInterface import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.models.ShellExecutionMode -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.onFailure @@ -67,19 +66,11 @@ class LazyActionErrorSnapshot( } private val isSystemBridgeConnected: Boolean by lazy { - if (buildConfigProvider.sdkInt >= Constants.SYSTEM_BRIDGE_MIN_API) { - systemBridgeConnectionManager.isConnected() - } else { - false - } + systemBridgeConnectionManager.isConnected() } private val keyEventActionsUseSystemBridge: Boolean by lazy { - if (buildConfigProvider.sdkInt >= Constants.SYSTEM_BRIDGE_MIN_API) { - preferenceRepository.get(Keys.keyEventActionsUseSystemBridge).firstBlocking() ?: false - } else { - false - } + preferenceRepository.get(Keys.keyEventActionsUseSystemBridge).firstBlocking() ?: false } override fun getErrors(actions: List): Map { @@ -129,8 +120,7 @@ class LazyActionErrorSnapshot( return isSupportedError } - if (buildConfigProvider.sdkInt >= Constants.SYSTEM_BRIDGE_MIN_API && - action is ActionData.InputKeyEvent && + if (action is ActionData.InputKeyEvent && keyEventActionsUseSystemBridge ) { if (!isSystemBridgeConnected) { @@ -155,8 +145,7 @@ class LazyActionErrorSnapshot( } @SuppressLint("NewApi") - if (buildConfigProvider.sdkInt >= Constants.SYSTEM_BRIDGE_MIN_API && - ActionUtils.isSystemBridgeRequired(action.id) && + if (ActionUtils.isSystemBridgeRequired(action.id) && !isSystemBridgeConnected ) { return SystemBridgeError.Disconnected @@ -241,6 +230,7 @@ class LazyActionErrorSnapshot( null } } + SettingType.SECURE, SettingType.GLOBAL, -> { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index 14660ca1b4..a5bfaa6454 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.base.actions import android.content.pm.PackageManager import android.os.Build import androidx.annotation.DrawableRes -import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack @@ -96,8 +95,6 @@ import io.github.sds100.keymapper.system.permissions.Permission object ActionUtils { - val isSystemBridgeSupported = Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API - @StringRes fun getCategoryLabel(category: ActionCategory): Int = when (category) { ActionCategory.NAVIGATION -> R.string.action_cat_navigation @@ -750,42 +747,44 @@ object ActionUtils { else -> emptyList() } - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) fun isSystemBridgeRequired(id: ActionId): Boolean { + // Actions are only tested on Android 10 and higher. return when (id) { ActionId.ENABLE_WIFI, ActionId.DISABLE_WIFI, ActionId.TOGGLE_WIFI, - -> true + -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ActionId.TOGGLE_MOBILE_DATA, ActionId.ENABLE_MOBILE_DATA, ActionId.DISABLE_MOBILE_DATA, - -> true + -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ActionId.TOGGLE_HOTSPOT, ActionId.ENABLE_HOTSPOT, ActionId.DISABLE_HOTSPOT, - -> true + -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, ActionId.TOGGLE_NFC, - -> true + -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ActionId.TOGGLE_AIRPLANE_MODE, ActionId.ENABLE_AIRPLANE_MODE, ActionId.DISABLE_AIRPLANE_MODE, - -> true + -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ActionId.TOGGLE_BLUETOOTH, ActionId.ENABLE_BLUETOOTH, ActionId.DISABLE_BLUETOOTH, -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 - ActionId.POWER_ON_OFF_DEVICE -> true + ActionId.POWER_ON_OFF_DEVICE -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - ActionId.FORCE_STOP_APP, ActionId.CLEAR_RECENT_APP -> true + ActionId.FORCE_STOP_APP, ActionId.CLEAR_RECENT_APP -> + Build.VERSION.SDK_INT >= + Build.VERSION_CODES.Q else -> false } @@ -796,7 +795,7 @@ object ActionUtils { ActionId.TOGGLE_MOBILE_DATA, ActionId.ENABLE_MOBILE_DATA, ActionId.DISABLE_MOBILE_DATA, - -> return if (isSystemBridgeSupported) { + -> return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { emptyList() } else { listOf(Permission.ROOT) @@ -869,7 +868,7 @@ object ActionUtils { ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, ActionId.TOGGLE_NFC, - -> return if (isSystemBridgeSupported) { + -> return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { emptyList() } else { listOf(Permission.ROOT) @@ -887,7 +886,7 @@ object ActionUtils { ActionId.TOGGLE_AIRPLANE_MODE, ActionId.ENABLE_AIRPLANE_MODE, ActionId.DISABLE_AIRPLANE_MODE, - -> return if (isSystemBridgeSupported) { + -> return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { emptyList() } else { listOf(Permission.ROOT) @@ -903,11 +902,12 @@ object ActionUtils { ActionId.SECURE_LOCK_DEVICE -> return listOf(Permission.DEVICE_ADMIN) - ActionId.POWER_ON_OFF_DEVICE -> return if (isSystemBridgeSupported) { - emptyList() - } else { - listOf(Permission.ROOT) - } + ActionId.POWER_ON_OFF_DEVICE -> + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + emptyList() + } else { + listOf(Permission.ROOT) + } ActionId.DISMISS_ALL_NOTIFICATIONS, ActionId.DISMISS_MOST_RECENT_NOTIFICATION, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModel.kt index e8262c6c42..d70ce81a7e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModel.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.base.actions -import android.os.Build import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -15,7 +14,6 @@ import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.models.ShellExecutionMode import io.github.sds100.keymapper.common.models.isExecuting -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.handle import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository @@ -45,16 +43,14 @@ class ConfigShellCommandViewModel @Inject constructor( init { // Update ExpertModeStatus in state - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - viewModelScope.launch { - systemBridgeConnectionManager.connectionState.map { connectionState -> - when (connectionState) { - is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED - is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED - } - }.collect { expertModeStatus -> - state = state.copy(expertModeStatus = expertModeStatus) + viewModelScope.launch { + systemBridgeConnectionManager.connectionState.map { connectionState -> + when (connectionState) { + is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED + is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED } + }.collect { expertModeStatus -> + state = state.copy(expertModeStatus = expertModeStatus) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ExecuteShellCommandUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ExecuteShellCommandUseCase.kt index cffaf1b859..26e096ee28 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ExecuteShellCommandUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ExecuteShellCommandUseCase.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.base.actions -import android.os.Build import io.github.sds100.keymapper.common.models.ShellExecutionMode import io.github.sds100.keymapper.common.models.ShellResult import io.github.sds100.keymapper.common.utils.KMError @@ -58,19 +57,15 @@ class ExecuteShellCommandUseCase @Inject constructor( command: String, timeoutMillis: Long, ): KMResult { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - runInterruptible(Dispatchers.IO) { - try { - systemBridgeConnectionManager.run { systemBridge -> - systemBridge.executeCommand(command, timeoutMillis) - } - // Only some standard exceptions can be thrown across Binder. - } catch (e: IllegalStateException) { - KMError.ShellCommandTimeout(timeoutMillis, null) + return runInterruptible(Dispatchers.IO) { + try { + systemBridgeConnectionManager.run { systemBridge -> + systemBridge.executeCommand(command, timeoutMillis) } + // Only some standard exceptions can be thrown across Binder. + } catch (e: IllegalStateException) { + KMError.ShellCommandTimeout(timeoutMillis, null) } - } else { - KMError.SdkVersionTooLow(Build.VERSION_CODES.Q) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt index f8ee5083f8..cac4b81f64 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt @@ -1,10 +1,8 @@ package io.github.sds100.keymapper.base.actions -import android.os.Build import io.github.sds100.keymapper.base.actions.sound.SoundsManager import io.github.sds100.keymapper.base.system.inputmethod.SwitchImeInterface import io.github.sds100.keymapper.common.BuildConfigProvider -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager @@ -20,7 +18,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -46,14 +43,10 @@ class GetActionErrorUseCaseImpl @Inject constructor( permissionAdapter.onPermissionsUpdate, soundsManager.soundFiles.drop(1).map { }, packageManagerAdapter.onPackagesChanged, - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - merge( - systemBridgeConnectionManager.connectionState.drop(1).map { }, - preferenceRepository.get(Keys.keyEventActionsUseSystemBridge), - ) - } else { - emptyFlow() - }, + merge( + systemBridgeConnectionManager.connectionState.drop(1).map { }, + preferenceRepository.get(Keys.keyEventActionsUseSystemBridge), + ), ) override val actionErrorSnapshot: Flow = channelFlow { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 2cb5589e1f..dc4d735993 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -21,7 +21,6 @@ import io.github.sds100.keymapper.base.system.navigation.OpenMenuHelper import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMError.SdkVersionTooLow @@ -879,9 +878,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.ScreenOnOff -> { - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API && - systemBridgeConnectionManager.isConnected() - ) { + if (systemBridgeConnectionManager.isConnected()) { val model = InjectKeyEventModel( keyCode = KeyEvent.KEYCODE_POWER, action = KeyEvent.ACTION_DOWN, @@ -1023,12 +1020,10 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( if (packageName == null) { result = KMError.Exception(Exception("No foreground app found to kill")) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + } else { result = systemBridgeConnectionManager.run { systemBridge -> systemBridge.forceStopPackage(packageName) } - } else { - result = SdkVersionTooLow(minSdk = Constants.SYSTEM_BRIDGE_MIN_API) } } @@ -1046,7 +1041,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( systemBridge.removeTasks(packageName) } } else { - result = SdkVersionTooLow(minSdk = Constants.SYSTEM_BRIDGE_MIN_API) + result = SdkVersionTooLow(minSdk = Build.VERSION_CODES.Q) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionDelegate.kt index 61f8cbe818..c555f586f9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionDelegate.kt @@ -11,7 +11,6 @@ import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults @@ -53,15 +52,11 @@ class FixKeyEventActionDelegateImpl @Inject constructor( NavigationProvider by navigationProvider { private val expertModeStatus: Flow = - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - systemBridgeConnectionManager.connectionState.map { state -> - when (state) { - is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED - is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED - } + systemBridgeConnectionManager.connectionState.map { state -> + when (state) { + is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED + is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED } - } else { - flowOf(ExpertModeStatus.UNSUPPORTED) } private val isExpertModeSelected: Flow = diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt index 14be776f2a..e98a45c12a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.expertmode import android.annotation.SuppressLint import android.os.Build -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import io.github.sds100.keymapper.base.BaseMainActivity import io.github.sds100.keymapper.base.BuildConfig @@ -13,7 +12,6 @@ import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.notifications.KMNotificationAction import io.github.sds100.keymapper.common.utils.Clock -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository @@ -53,7 +51,6 @@ import timber.log.Timber * This class handles auto starting the system bridge when Key Mapper is launched and when * the System Bridge is killed not due to the user. */ -@RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) @Singleton class SystemBridgeAutoStarter @Inject constructor( private val coroutineScope: CoroutineScope, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt index d823175d9e..1cba44467a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt @@ -19,7 +19,6 @@ import io.github.sds100.keymapper.base.system.notifications.NotificationControll import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.KeyMapperClassProvider import io.github.sds100.keymapper.common.notifications.KMNotificationAction -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess @@ -44,7 +43,6 @@ import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber @Suppress("KotlinConstantConditions") -@RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) class SystemBridgeSetupAssistantController @AssistedInject constructor( @Assisted private val coroutineScope: CoroutineScope, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt index 7caf46f401..574c10f329 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt @@ -5,7 +5,6 @@ import android.os.Process import androidx.annotation.RequiresApi import dagger.hilt.android.scopes.ViewModelScoped import io.github.sds100.keymapper.common.utils.Clock -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.data.Keys @@ -33,7 +32,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @OptIn(ExperimentalCoroutinesApi::class) -@RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) @ViewModelScoped class SystemBridgeSetupUseCaseImpl @Inject constructor( private val preferences: PreferenceRepository, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevDevicesDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevDevicesDelegate.kt index 93b77346eb..efdac6554d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevDevicesDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevDevicesDelegate.kt @@ -1,10 +1,8 @@ package io.github.sds100.keymapper.base.input -import androidx.annotation.RequiresApi import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.models.GrabTargetKeyCode import io.github.sds100.keymapper.common.models.GrabbedDeviceHandle -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.valueIfFailure import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager @@ -27,7 +25,6 @@ import timber.log.Timber * device name introduces extra overhead across Binder and JNI. */ @OptIn(ExperimentalCoroutinesApi::class) -@RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) @Singleton class EvdevDevicesDelegate @Inject constructor( private val coroutineScope: CoroutineScope, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index 3feaad3905..b1615136e6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -1,14 +1,11 @@ package io.github.sds100.keymapper.base.input -import android.os.Build import android.view.KeyEvent -import androidx.annotation.RequiresApi import io.github.sds100.keymapper.base.BuildConfig import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.models.GrabTargetKeyCode import io.github.sds100.keymapper.common.models.GrabbedDeviceHandle -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success @@ -73,19 +70,17 @@ class InputEventHubImpl @Inject constructor( init { startKeyEventProcessingLoop() - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - coroutineScope.launch { - systemBridgeConnManager.connectionState - .filterIsInstance() - .collect { - // Whenever the system bridge is connected - systemBridgeConnManager.run { bridge -> - bridge.registerEvdevCallback(this@InputEventHubImpl) - }.onSuccess { - invalidateGrabbedDevices() - } + coroutineScope.launch { + systemBridgeConnManager.connectionState + .filterIsInstance() + .collect { + // Whenever the system bridge is connected + systemBridgeConnManager.run { bridge -> + bridge.registerEvdevCallback(this@InputEventHubImpl) + }.onSuccess { + invalidateGrabbedDevices() } - } + } } } @@ -104,12 +99,10 @@ class InputEventHubImpl @Inject constructor( } } - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) override fun isSystemBridgeConnected(): Boolean { return systemBridgeConnManager.isConnected() } - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) override fun onEvdevEvent( deviceId: Int, timeSec: Long, @@ -221,20 +214,15 @@ class InputEventHubImpl @Inject constructor( } clients[clientId] = ClientContext(callback, emptySet(), evdevEventTypes.toSet()) - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - invalidateGrabbedDevices() - } + invalidateGrabbedDevices() } override fun unregisterClient(clientId: String) { Timber.d("InputEventHub: Unregistering client $clientId") clients.remove(clientId) - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - invalidateGrabbedDevices() - } + invalidateGrabbedDevices() } - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) override fun setGrabTargets(clientId: String, devices: List) { if (!clients.containsKey(clientId)) { throw IllegalArgumentException( @@ -246,12 +234,10 @@ class InputEventHubImpl @Inject constructor( invalidateGrabbedDevices() } - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) override fun getGrabbedDevices(): List { return evdevDevicesDelegate.getGrabbedDevices() } - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) override fun grabAllEvdevDevices(clientId: String) { if (!clients.containsKey(clientId)) { throw IllegalArgumentException( @@ -274,7 +260,6 @@ class InputEventHubImpl @Inject constructor( invalidateGrabbedDevices() } - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) override fun injectEvdevEvent( deviceId: Int, type: Int, @@ -300,7 +285,6 @@ class InputEventHubImpl @Inject constructor( } } - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) override fun injectEvdevEventKeyCode(deviceId: Int, keyCode: Int, value: Int): KMResult { return systemBridgeConnManager.run { bridge -> bridge.writeEvdevEventKeyCode( @@ -324,10 +308,7 @@ class InputEventHubImpl @Inject constructor( event: InjectKeyEventModel, useSystemBridgeIfAvailable: Boolean, ): KMResult { - val isSysBridgeConnected = Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API && - systemBridgeConnManager.connectionState.value is SystemBridgeConnectionState.Connected - - if (isSysBridgeConnected && useSystemBridgeIfAvailable) { + if (systemBridgeConnManager.isConnected() && useSystemBridgeIfAvailable) { val androidKeyEvent = event.toAndroidKeyEvent(flags = KeyEvent.FLAG_FROM_SYSTEM) if (logInputEventsEnabled.value) { @@ -371,19 +352,16 @@ class InputEventHubImpl @Inject constructor( .firstBlocking() } - @RequiresApi(Build.VERSION_CODES.Q) override fun onGrabbedDevicesChanged(devices: Array?) { val devicesList = devices?.filterNotNull()?.toList() ?: emptyList() evdevDevicesDelegate.onGrabbedDevicesChanged(devicesList) } - @RequiresApi(Build.VERSION_CODES.Q) override fun onEvdevDevicesChanged(devices: Array?) { val devicesList = devices?.filterNotNull()?.toList() ?: emptyList() evdevDevicesDelegate.onEvdevDevicesChanged(devicesList) } - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) private fun invalidateGrabbedDevices() { val devicesToGrab = clients.values.flatMap { it.grabRequests }.toSet() evdevDevicesDelegate.setGrabTargets(devicesToGrab.toList()) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt index 6bf7f2ca6a..b628f973af 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt @@ -1,7 +1,6 @@ package io.github.sds100.keymapper.base.keymaps import android.graphics.drawable.Drawable -import android.os.Build import dagger.hilt.android.scopes.ViewModelScoped import io.github.sds100.keymapper.base.actions.DisplayActionUseCase import io.github.sds100.keymapper.base.actions.GetActionErrorUseCase @@ -49,7 +48,6 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart @@ -105,18 +103,10 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( } private val systemBridgeConnectionState: Flow = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - systemBridgeConnectionManager.connectionState - } else { - flowOf(null) - } + systemBridgeConnectionManager.connectionState private val evdevDevices: Flow?> = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - grabbedEvdevDeviceCache.allDevices - } else { - flowOf(null) - } + grabbedEvdevDeviceCache.allDevices /** * Cache the data required for checking errors to reduce the latency of repeatedly checking @@ -186,7 +176,8 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( TriggerError.FLOATING_BUTTON_DELETED, TriggerError.SYSTEM_BRIDGE_UNSUPPORTED, TriggerError.MIGRATE_SCREEN_OFF_TRIGGER, - -> {} + -> { + } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/SystemBridgeLogger.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/SystemBridgeLogger.kt index 1deffaf591..9c4ee7f99e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/SystemBridgeLogger.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/SystemBridgeLogger.kt @@ -1,8 +1,6 @@ package io.github.sds100.keymapper.base.logging import android.util.Log -import androidx.annotation.RequiresApi -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.sysbridge.ILogCallback @@ -25,7 +23,6 @@ import timber.log.Timber * preference to control the log level. */ @Singleton -@RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) class SystemBridgeLogger @Inject constructor( private val coroutineScope: CoroutineScope, private val systemBridgeConnManager: SystemBridgeConnectionManager, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt index 9b3f8bd802..d3ab87eed6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.base.onboarding -import android.os.Build import android.view.KeyEvent import dagger.hilt.android.scopes.ViewModelScoped import io.github.sds100.keymapper.base.R @@ -16,7 +15,6 @@ import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository @@ -341,8 +339,7 @@ class OnboardingTipDelegateImpl @Inject constructor( } if (hasRingerModeAction && - !shownRingerModeTip && - Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API + !shownRingerModeTip ) { val tip = OnboardingTipModel( id = RINGER_MODE_TIP_ID, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 9cbcb0ed7a..5d79cd6838 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -72,11 +72,9 @@ import io.github.sds100.keymapper.base.utils.ui.compose.icons.FolderManaged import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons import io.github.sds100.keymapper.base.utils.ui.compose.icons.WandStars import io.github.sds100.keymapper.common.utils.BuildUtils -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.system.files.FileUtils import kotlinx.coroutines.launch -private val isExpertModeSupported = Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API private val isAutoSwitchImeSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R @Composable @@ -149,20 +147,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onThemeSelected = viewModel::onThemeSelected, onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick, onDefaultOptionsClick = viewModel::onDefaultOptionsClick, - onExpertModeClick = { - if (isExpertModeSupported) { - viewModel.onExpertModeClick() - } else { - scope.launch { - snackbarHostState.showSnackbar( - context.getString( - R.string.error_sdk_version_too_low, - BuildUtils.getSdkVersionName(Constants.SYSTEM_BRIDGE_MIN_API), - ), - ) - } - } - }, + onExpertModeClick = viewModel::onExpertModeClick, onAutomaticChangeImeClick = viewModel::onAutomaticChangeImeClick, onForceVibrateToggled = viewModel::onForceVibrateToggled, onLoggingToggled = viewModel::onLoggingToggled, @@ -365,17 +350,9 @@ private fun Content( OptionPageButton( title = stringResource(R.string.title_pref_expert_mode), - text = if (isExpertModeSupported) { - stringResource(R.string.summary_pref_expert_mode) - } else { - stringResource( - R.string.error_sdk_version_too_low, - BuildUtils.getSdkVersionName(Constants.SYSTEM_BRIDGE_MIN_API), - ) - }, + text = stringResource(R.string.summary_pref_expert_mode), icon = Icons.Outlined.OfflineBolt, onClick = onExpertModeClick, - enabled = isExpertModeSupported, ) OptionPageButton( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index a713408d13..8d6bef80fd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -25,7 +25,6 @@ import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent import io.github.sds100.keymapper.base.system.inputmethod.AutoSwitchImeController import io.github.sds100.keymapper.base.trigger.RecordTriggerController -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.minusFlag @@ -107,12 +106,8 @@ abstract class BaseAccessibilityServiceController( val accessibilityNodeRecorder = accessibilityNodeRecorderFactory.create(service) - private val setupAssistantController: SystemBridgeSetupAssistantController? = - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - setupAssistantControllerFactory.create(service.lifecycleScope, service) - } else { - null - } + private val setupAssistantController: SystemBridgeSetupAssistantController = + setupAssistantControllerFactory.create(service.lifecycleScope, service) private val autoSwitchImeController: AutoSwitchImeController? by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -362,9 +357,7 @@ abstract class BaseAccessibilityServiceController( relayServiceCallback, ) - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - setupAssistantController?.onServiceConnected() - } + setupAssistantController.onServiceConnected() } open fun onDestroy() { @@ -372,9 +365,7 @@ abstract class BaseAccessibilityServiceController( keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_ACCESSIBILITY_SERVICE) accessibilityNodeRecorder.teardown() - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - setupAssistantController?.teardown() - } + setupAssistantController.teardown() } open fun onConfigurationChanged(newConfig: Configuration) { @@ -425,9 +416,7 @@ abstract class BaseAccessibilityServiceController( autoSwitchImeController?.onAccessibilityEvent(event) } - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - setupAssistantController?.onAccessibilityEvent(event) - } + setupAssistantController.onAccessibilityEvent(event) } @RequiresApi(Build.VERSION_CODES.TIRAMISU) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index 31054d496d..19769a183a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.base.system.notifications -import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import io.github.sds100.keymapper.base.BaseMainActivity @@ -13,7 +12,6 @@ import io.github.sds100.keymapper.base.system.inputmethod.ToggleCompatibleImeUse import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.notifications.KMNotificationAction -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.DefaultDispatcherProvider import io.github.sds100.keymapper.common.utils.DispatcherProvider import io.github.sds100.keymapper.common.utils.onFailure @@ -191,15 +189,13 @@ class NotificationController @Inject constructor( } }.launchIn(coroutineScope) - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - coroutineScope.launch { - systemBridgeConnectionManager.connectionState - .collect { connectionState -> - if (connectionState is SystemBridgeConnectionState.Connected) { - showSystemBridgeStartedNotification() - } + coroutineScope.launch { + systemBridgeConnectionManager.connectionState + .collect { connectionState -> + if (connectionState is SystemBridgeConnectionState.Connected) { + showSystemBridgeStartedNotification() } - } + } } coroutineScope.launch { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 52f09bbe3f..bb96f39815 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.base.trigger -import android.os.Build import android.view.KeyEvent import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -115,31 +114,21 @@ abstract class BaseConfigTriggerViewModel( ) val expertModeSwitchState: StateFlow = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - combine( - recordTrigger.state, - recordTrigger.isEvdevRecordingEnabled, - systemBridgeConnectionManager.connectionState, - Companion::buildExpertModeSwitchState, - ) - .stateIn( - viewModelScope, - SharingStarted.Eagerly, - ExpertModeRecordSwitchState( - isVisible = false, - isChecked = false, - isEnabled = false, - ), - ) - } else { - MutableStateFlow( + combine( + recordTrigger.state, + recordTrigger.isEvdevRecordingEnabled, + systemBridgeConnectionManager.connectionState, + Companion::buildExpertModeSwitchState, + ) + .stateIn( + viewModelScope, + SharingStarted.Eagerly, ExpertModeRecordSwitchState( isVisible = false, isChecked = false, isEnabled = false, ), ) - } val showFingerprintGesturesShortcut: StateFlow = fingerprintGesturesSupported.isSupported.map { it ?: false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupDelegate.kt index 9eaec3e10c..15e0ab63a7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupDelegate.kt @@ -11,7 +11,6 @@ import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.AccessibilityServiceError -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess @@ -68,15 +67,11 @@ class TriggerSetupDelegateImpl @Inject constructor( MutableStateFlow(TriggerSetupState.Gamepad.Type.DPAD) private val expertModeStatus: Flow = - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - systemBridgeConnectionManager.connectionState.map { state -> - when (state) { - is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED - is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED - } + systemBridgeConnectionManager.connectionState.map { state -> + when (state) { + is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED + is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED } - } else { - flowOf(ExpertModeStatus.UNSUPPORTED) } override val triggerSetupState: StateFlow = @@ -312,14 +307,10 @@ class TriggerSetupDelegateImpl @Inject constructor( serviceState == AccessibilityServiceState.ENABLED && expertModeStatus == ExpertModeStatus.ENABLED - val remapStatus = if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - if (areRequirementsMet) { - RemapStatus.SUPPORTED - } else { - RemapStatus.UNCERTAIN - } + val remapStatus = if (areRequirementsMet) { + RemapStatus.SUPPORTED } else { - RemapStatus.UNSUPPORTED + RemapStatus.UNCERTAIN } TriggerSetupState.Power( @@ -342,14 +333,10 @@ class TriggerSetupDelegateImpl @Inject constructor( serviceState == AccessibilityServiceState.ENABLED && expertModeStatus == ExpertModeStatus.ENABLED - val remapStatus = if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { - if (areRequirementsMet) { - RemapStatus.SUPPORTED - } else { - RemapStatus.UNCERTAIN - } + val remapStatus = if (areRequirementsMet) { + RemapStatus.SUPPORTED } else { - RemapStatus.UNSUPPORTED + RemapStatus.UNCERTAIN } TriggerSetupState.Mouse( diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/Constants.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/Constants.kt index d06a8fe6fc..2dcf8546bf 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/Constants.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/Constants.kt @@ -5,5 +5,4 @@ import android.os.Build object Constants { const val MIN_API: Int = Build.VERSION_CODES.O const val MAX_API: Int = 1000 - const val SYSTEM_BRIDGE_MIN_API = Build.VERSION_CODES.Q } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 12453dca8d..9e92af19f8 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -17,7 +17,6 @@ import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.models.isSuccess -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils @@ -297,25 +296,18 @@ interface SystemBridgeConnectionManager { // Do not require min API to check the state. val connectionState: StateFlow - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) fun run(block: (ISystemBridge) -> T): KMResult - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) fun stopSystemBridge() - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) fun restartSystemBridge() - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) suspend fun startWithRoot() - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) fun startWithShizuku() - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) suspend fun startWithAdb() - @RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) suspend fun getShellStartCommand(): KMResult } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index cc6f0b247f..337b20cba6 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -1,7 +1,6 @@ package io.github.sds100.keymapper.sysbridge.service import android.Manifest -import android.annotation.SuppressLint import android.app.ActivityManager import android.content.ActivityNotFoundException import android.content.ComponentName @@ -16,7 +15,6 @@ import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.KeyMapperClassProvider -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.isSuccess @@ -421,8 +419,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } -@SuppressLint("ObsoleteSdkInt") -@RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API) interface SystemBridgeSetupController { val setupAssistantStep: Flow val isStarting: StateFlow diff --git a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt index 00cbe0cff3..57d44fb979 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt @@ -4,7 +4,6 @@ import android.content.Context import android.os.Build import android.provider.Settings import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils @@ -25,7 +24,7 @@ class AndroidAirplaneModeAdapter @Inject constructor( ) : AirplaneModeAdapter { override suspend fun enable(): KMResult<*> { - return if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { systemBridgeConnectionManager.run { bridge -> bridge.setAirplaneMode(true) } } else { val success = SettingsUtils.putGlobalSetting(ctx, Settings.Global.AIRPLANE_MODE_ON, 1) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index 78545c412b..f500d1d980 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -22,7 +22,6 @@ import androidx.core.content.ContextCompat import androidx.core.content.IntentCompat import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success @@ -171,7 +170,7 @@ class AndroidNetworkAdapter @Inject constructor( override fun isWifiEnabledFlow(): Flow = isWifiEnabled override fun enableWifi(): KMResult<*> { - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return systemBridgeConnManager.run { bridge -> bridge.setWifiEnabled(true) } } else { wifiManager.isWifiEnabled = true @@ -180,7 +179,7 @@ class AndroidNetworkAdapter @Inject constructor( } override fun disableWifi(): KMResult<*> { - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return systemBridgeConnManager.run { bridge -> bridge.setWifiEnabled(false) } } else { wifiManager.isWifiEnabled = false @@ -200,7 +199,7 @@ class AndroidNetworkAdapter @Inject constructor( throw IllegalStateException("No valid subscription ID") } - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return systemBridgeConnManager.run { bridge -> bridge.setDataEnabled(subId, true) } } else { return runBlocking { suAdapter.execute("svc data enable") } @@ -214,7 +213,7 @@ class AndroidNetworkAdapter @Inject constructor( throw IllegalStateException("No valid subscription ID") } - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return systemBridgeConnManager.run { bridge -> bridge.setDataEnabled(subId, false) } } else { return runBlocking { suAdapter.execute("svc data disable") } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt index a57a5c3462..f60e87f54c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt @@ -5,7 +5,6 @@ import android.nfc.NfcManager import android.os.Build import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter @@ -26,7 +25,7 @@ class AndroidNfcAdapter @Inject constructor( override fun isEnabled(): Boolean = nfcManager?.defaultAdapter?.isEnabled ?: false override fun enable(): KMResult<*> { - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return systemBridgeConnectionManager.run { bridge -> bridge.setNfcEnabled(true) } } else { return runBlocking { suAdapter.execute("svc nfc enable") } @@ -34,7 +33,7 @@ class AndroidNfcAdapter @Inject constructor( } override fun disable(): KMResult<*> { - if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return systemBridgeConnectionManager.run { bridge -> bridge.setNfcEnabled(false) } } else { return runBlocking { suAdapter.execute("svc nfc disable") } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt index 3ee62e2f59..fe7f925e70 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt @@ -18,7 +18,6 @@ import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.firstBlocking @@ -170,11 +169,7 @@ class AndroidPermissionAdapter @Inject constructor( -1 } - val isSystemBridgeConnected = - Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API && - systemBridgeConnectionManager.isConnected() - - if (isSystemBridgeConnected) { + if (systemBridgeConnectionManager.isConnected()) { result = systemBridgeConnectionManager.run { bridge -> bridge.grantPermission(permissionName, deviceId) }.then { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt index ff871199ca..8a7e3ea0eb 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt @@ -7,7 +7,6 @@ import android.os.Build import android.provider.Settings import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.Success @@ -47,8 +46,11 @@ class AndroidVolumeAdapter @Inject constructor( return when (ringerModeSdk) { AudioManager.RINGER_MODE_NORMAL -> RingerMode.NORMAL + AudioManager.RINGER_MODE_VIBRATE -> RingerMode.VIBRATE + AudioManager.RINGER_MODE_SILENT -> RingerMode.SILENT + else -> throw Exception( "Don't know how to convert this ringer moder ${audioManager.ringerMode}", ) @@ -97,7 +99,7 @@ class AndroidVolumeAdapter @Inject constructor( RingerMode.SILENT -> AudioManager.RINGER_MODE_SILENT } - return if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { systemBridgeConnectionManager.run { systemBridge -> systemBridge.setRingerMode(sdkMode) }.otherwise { @@ -155,7 +157,6 @@ class AndroidVolumeAdapter @Inject constructor( VolumeStream.SYSTEM -> Success(AudioManager.STREAM_SYSTEM) VolumeStream.VOICE_CALL -> Success(AudioManager.STREAM_VOICE_CALL) VolumeStream.ACCESSIBILITY -> Success(AudioManager.STREAM_ACCESSIBILITY) - null -> Success(null) } From 529c31a95e0f92e66b2bc6c830019757aa19081b Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 11 Feb 2026 15:43:49 +0100 Subject: [PATCH 03/17] add .idea modules --- evdev/.idea/modules.xml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 evdev/.idea/modules.xml diff --git a/evdev/.idea/modules.xml b/evdev/.idea/modules.xml new file mode 100644 index 0000000000..56152584b9 --- /dev/null +++ b/evdev/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file From cc03f276b962103eb9970a14de96b0196b4e001e Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 11 Feb 2026 16:30:39 +0100 Subject: [PATCH 04/17] fix: use new domain for contact email --- base/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 1144fc4bc3..89faf244b6 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1396,7 +1396,7 @@ Contact developer You must purchase the side key trigger feature! Tap on the key map and then purchase it by clicking on the shop. You must purchase the floating buttons feature! Tap on the key map and then purchase it by clicking on the shop. - contact@keymapper.club + contact@keymapper.app Key Mapper Pro query Please fill the following information so I can help you.\n\n1. Device model:\n2. Android version:\n3. Key maps (make a backup in the home screen menu):\n4. Screenshot of Key Mapper home screen:\n5. Describe the problem you are having: Thank you for supporting the app! From 413c6de303ff664b1db3d5798348e020d2a4250e Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 11 Feb 2026 16:31:31 +0100 Subject: [PATCH 05/17] fix: use new domain in issue template --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 165131bc55..a28125fdec 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Need help using the app? - url: http://keymapper.club?utm_source=issue_template_chooser + url: http://keymapper.app/discord?utm_source=issue_template_chooser about: Join the Discord server and let us help you \ No newline at end of file From 786fad22e09852e51b0fd14451320629c3dbb5be Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 11 Feb 2026 16:53:20 +0100 Subject: [PATCH 06/17] #2025 feat: add button to report bug on home screen --- CHANGELOG.md | 1 + .../base/home/HomeKeyMapListScreen.kt | 50 +++++++++++++++++++ .../keymapper/base/home/KeyMapListAppBar.kt | 25 +++++++--- .../sds100/keymapper/base/utils/ShareUtils.kt | 27 +++++++++- base/src/main/res/values/strings.xml | 10 +++- 5 files changed, 102 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ada825bf..584dde8586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## Added - #2024 support Expert mode on all Android versions supported by Key Mapper (8.0+). +- #2025 add report bug button to home screen menu. ## [4.0.3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.3) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt index c5b71421a0..964825e123 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.only @@ -24,12 +25,15 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowForward import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.FlashlightOn +import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -145,6 +149,12 @@ fun HomeKeyMapListScreen( ) } + var showBugReportDialog by rememberSaveable { mutableStateOf(false) } + + if (showBugReportDialog) { + BugReportDialog(onDismissRequest = { showBugReportDialog = false }) + } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val uriHandler = LocalUriHandler.current val ctx = LocalContext.current @@ -216,6 +226,9 @@ fun HomeKeyMapListScreen( onConstraintModeChanged = viewModel::onGroupConstraintModeChanged, onFixConstraintClick = viewModel::onFixClick, onKeyMapsEnabledChange = viewModel::onGroupKeyMapsEnabledChanged, + onReportBugClick = { + showBugReportDialog = true + }, ) }, selectionBottomSheet = { @@ -349,6 +362,43 @@ fun HandleImportExportState( } } +@Composable +private fun BugReportDialog(onDismissRequest: () -> Unit) { + val ctx = LocalContext.current + val subject = stringResource(R.string.customer_email_subject_bug_report) + + val discordLink = stringResource(R.string.url_discord_server_invite) + val uriHandler = LocalUriHandler.current + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(stringResource(R.string.dialog_title_bug_report)) }, + text = { Text(stringResource(R.string.dialog_message_bug_report)) }, + confirmButton = { + Row { + TextButton( + onClick = { + ShareUtils.sendBugReportEmail(ctx, subject) + }, + ) { + Text(stringResource(R.string.dialog_bug_report_email_button)) + } + TextButton( + onClick = { + uriHandler.openUriSafe(ctx, discordLink) + }, + ) { + Text(stringResource(R.string.dialog_bug_report_discord_button)) + } + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.neg_cancel)) + } + }, + ) +} + @Composable private fun sampleList(): List { val context = LocalContext.current diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt index 4e047294e4..cd64514d4e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt @@ -37,6 +37,7 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.HelpOutline import androidx.compose.material.icons.automirrored.rounded.Sort import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.rounded.BugReport import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Done import androidx.compose.material.icons.rounded.Edit @@ -138,6 +139,7 @@ fun KeyMapListAppBar( onConstraintModeChanged: (ConstraintMode) -> Unit = {}, onFixConstraintClick: (KMError) -> Unit = {}, onKeyMapsEnabledChange: (Boolean) -> Unit = {}, + onReportBugClick: () -> Unit = {}, ) { BackHandler(onBack = onBackClick) @@ -170,10 +172,6 @@ fun KeyMapListAppBar( dropdownMenuContent = { RootGroupDropdownMenu( expanded = expandedDropdown, - onSortClick = { - expandedDropdown = false - onSortClick() - }, onSettingsClick = { expandedDropdown = false onSettingsClick() @@ -194,6 +192,10 @@ fun KeyMapListAppBar( expandedDropdown = false onInputMethodPickerClick() }, + onReportBugClick = { + expandedDropdown = false + onReportBugClick() + }, onDismissRequest = { expandedDropdown = false }, ) }, @@ -531,9 +533,11 @@ private fun ChildGroupAppBar( SelectedKeyMapsEnabled.ALL -> stringResource( R.string.home_enabled_key_maps_enabled, ) + SelectedKeyMapsEnabled.MIXED -> stringResource( R.string.home_enabled_key_maps_mixed, ) + SelectedKeyMapsEnabled.NONE, null -> stringResource( R.string.home_enabled_key_maps_disabled, ) @@ -871,12 +875,12 @@ private fun selectedTextTransition(targetState: Int, initialState: Int): Content @Composable private fun RootGroupDropdownMenu( expanded: Boolean, - onSortClick: () -> Unit = {}, onSettingsClick: () -> Unit = {}, onAboutClick: () -> Unit = {}, onExportClick: () -> Unit = {}, onImportClick: () -> Unit = {}, onInputMethodPickerClick: () -> Unit = {}, + onReportBugClick: () -> Unit = {}, onDismissRequest: () -> Unit = {}, ) { DropdownMenu( @@ -908,6 +912,11 @@ private fun RootGroupDropdownMenu( text = { Text(stringResource(R.string.home_menu_about)) }, onClick = onAboutClick, ) + DropdownMenuItem( + leadingIcon = { Icon(Icons.Rounded.BugReport, contentDescription = null) }, + text = { Text(stringResource(R.string.home_menu_report_bug)) }, + onClick = onReportBugClick, + ) } } @@ -1037,7 +1046,7 @@ private fun KeyMapsChildGroupDarkPreview() { @Preview @Composable private fun KeyMapsChildGroupEditingPreview() { - val focusRequester = FocusRequester() + val focusRequester = remember { FocusRequester() } LaunchedEffect("") { focusRequester.requestFocus() @@ -1075,7 +1084,7 @@ private fun KeyMapsChildGroupEditingDarkPreview() { keyMapsEnabled = SelectedKeyMapsEnabled.ALL, ) - val focusRequester = FocusRequester() + val focusRequester = remember { FocusRequester() } LaunchedEffect("") { focusRequester.requestFocus() @@ -1091,7 +1100,7 @@ private fun KeyMapsChildGroupEditingDarkPreview() { @Preview @Composable private fun KeyMapsChildGroupErrorPreview() { - val focusRequester = FocusRequester() + val focusRequester = remember { FocusRequester() } LaunchedEffect("") { focusRequester.requestFocus() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ShareUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ShareUtils.kt index 836188899d..d4e0bd5fb7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ShareUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ShareUtils.kt @@ -14,10 +14,35 @@ import io.github.sds100.keymapper.base.BaseMainActivity import io.github.sds100.keymapper.base.R object ShareUtils { + fun sendBugReportEmail(ctx: Context, subject: String) { + val body = ctx.getString( + R.string.customer_email_body, + Build.DEVICE, + if (Build.VERSION.SDK_INT >= + Build.VERSION_CODES.R + ) { + Build.VERSION.RELEASE_OR_CODENAME + } else { + Build.VERSION.RELEASE + }, + ctx.applicationContext.packageManager.getPackageInfo(ctx.packageName, 0).versionName, + ) + + sendMail( + ctx, + email = ctx.getString(R.string.purchasing_contact_email), + subject = subject, + body = body, + ) + } + fun sendMail(ctx: Context, email: String, subject: String, body: String) { try { + // Specify the extra parameters so it works with the gmail app. + val uri = "mailto:$email?subject=$subject&body=$body".toUri() + val intent = Intent(Intent.ACTION_SENDTO).apply { - data = "mailto:$email".toUri() + data = uri putExtra(Intent.EXTRA_SUBJECT, subject) putExtra(Intent.EXTRA_TEXT, body) } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 89faf244b6..8914225d0f 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1397,8 +1397,9 @@ You must purchase the side key trigger feature! Tap on the key map and then purchase it by clicking on the shop. You must purchase the floating buttons feature! Tap on the key map and then purchase it by clicking on the shop. contact@keymapper.app - Key Mapper Pro query - Please fill the following information so I can help you.\n\n1. Device model:\n2. Android version:\n3. Key maps (make a backup in the home screen menu):\n4. Screenshot of Key Mapper home screen:\n5. Describe the problem you are having: + Key Mapper Pro query + Key Mapper Bug report + Please fill the following information so I can help you.\n\n1. Device model: %s\n2. Android version: %s\n3. Key Mapper version: %s\n4, Key maps (make a backup in the home screen menu)\n6. Screenshot of Key Mapper home screen\n6. Describe the problem you are having Thank you for supporting the app! Your purchase was successful. As a paying user of Key\u00A0Mapper you will receive priority support to help you use the app. There is now a button in the shop to contact the developer. The advanced triggers are paid feature but you downloaded the FOSS build of Key Mapper that does not include this closed source module or Google Play billing. Please download Key Mapper from Google Play to get access to this feature. @@ -1518,6 +1519,7 @@ Settings Delete group About + Report bug Export all Import Choose keyboard @@ -1891,5 +1893,9 @@ We would love to know! Please consider leaving a review on Google Play to help others discover this feature. ❤️ Dismiss Write evdev event failed + Report bug + We would prefer it if you report your bug to the Discord server so other users can help you. Otherwise, you can email us at contact@keymapper.app. + Open Discord + Email us From 0a29daa5bac9033923cfa47fe17779fc66046e68 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 11 Feb 2026 21:14:35 +0100 Subject: [PATCH 07/17] #2030 fix: do not filter out unknown evdev key events --- CHANGELOG.md | 3 +++ .../main/rust/evdev_manager/core/src/event_loop.rs | 13 +++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 584dde8586..2de101a65f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - #2024 support Expert mode on all Android versions supported by Key Mapper (8.0+). - #2025 add report bug button to home screen menu. +## Fixed +- #2030 do not filter out unknown evdev key events. + ## [4.0.3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.3) #### 07 February 2026 diff --git a/evdev/src/main/rust/evdev_manager/core/src/event_loop.rs b/evdev/src/main/rust/evdev_manager/core/src/event_loop.rs index 25ab4126c5..c9ccb86001 100644 --- a/evdev/src/main/rust/evdev_manager/core/src/event_loop.rs +++ b/evdev/src/main/rust/evdev_manager/core/src/event_loop.rs @@ -394,10 +394,15 @@ impl EventLoopThread { // Key Mapper only cares about key events. Do not send other events so latency // isn't introduced with the IPC. let consumed = match event.event_code { - EventCode::EV_KEY(_) => { - self.callback - .on_evdev_event(device_id, &grabbed_device.device_info, event) - } + // See #2030. Some devices send unknown scan codes so still send them + // to Key Mapper. + EventCode::EV_KEY(_) + | EventCode::EV_UNK { + event_type: 1, + event_code: _, + } => self + .callback + .on_evdev_event(device_id, &grabbed_device.device_info, event), _ => false, }; From 93f40215924e3e8d060042c42d6563ddc100a82b Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 13 Feb 2026 16:35:27 +0100 Subject: [PATCH 08/17] #2027 make the sorting bottom sheet clearer to use --- CHANGELOG.md | 4 +++- .../base/sorting/SortBottomSheetContent.kt | 24 ++++++++++++------- base/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de101a65f..fc7665c05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,15 @@ ## [4.0.4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.4) -#### 11 February 2026 +#### TO BE RELEASED ## Added - #2024 support Expert mode on all Android versions supported by Key Mapper (8.0+). - #2025 add report bug button to home screen menu. +- #2027 Make the key map sorting feature easier to understand. ## Fixed + - #2030 do not filter out unknown evdev key events. ## [4.0.3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.3) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt index 066a14a68d..b4eb895661 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/SortBottomSheetContent.kt @@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowDownward import androidx.compose.material.icons.rounded.ArrowUpward import androidx.compose.material.icons.rounded.DragHandle +import androidx.compose.material.icons.rounded.HorizontalRule import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card @@ -62,7 +63,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset @@ -70,6 +70,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.base.utils.ui.compose.DraggableItem import io.github.sds100.keymapper.base.utils.ui.compose.rememberDragDropState import kotlinx.coroutines.CoroutineScope @@ -368,7 +369,11 @@ private fun SortFieldListItem( Text( text = sortFieldText(sortField), style = if (sortOrder == SortOrder.NONE) { - MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal) + MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.5f, + ), + ) } else { MaterialTheme.typography.titleMedium }, @@ -389,20 +394,23 @@ private fun SortFieldListItem( }, label = "$sortField Sort Order", ) { sortOrder -> - if (sortOrder == SortOrder.NONE) { - Spacer(Modifier.size(24.dp)) - return@AnimatedContent - } - val imageVector = when (sortOrder) { - SortOrder.NONE -> return@AnimatedContent + SortOrder.NONE -> Icons.Rounded.HorizontalRule SortOrder.ASCENDING -> Icons.Rounded.ArrowUpward SortOrder.DESCENDING -> Icons.Rounded.ArrowDownward } + val tint = when (sortOrder) { + SortOrder.NONE -> LocalCustomColorsPalette.current.red + + SortOrder.ASCENDING, + SortOrder.DESCENDING, + -> LocalCustomColorsPalette.current.green + } Icon( imageVector = imageVector, contentDescription = null, + tint = tint, ) } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 8914225d0f..534906a56b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -482,8 +482,8 @@ You will be taken to your device\'s settings page to manage which apps can modify the Do Not Disturb state. This is not present on some devices so tap don\'t show again if you do not see Key Mapper in the list. Sort by - Drag the handles to adjust priorities. The item at the top is the most important. You can also tap any item to reverse its sort order. - Example: To sort key maps primarily by their Actions in ascending order and secondarily by their Triggers in descending order, move Actions to the first position and Triggers to the second. + Drag the handles to adjust priorities. The item at the top is the most important. You must tap the item to enable sorting and toggle ascending/descending. + Example: To sort by Actions (ascending) first and Triggers (descending) second: tap and drag Actions to the first position and set it to ascending, then tap and drag Triggers to the second position and set it to descending. Drag handle for %1$s Show example From eff4bbf6a727e5fb9c482ea6be6584383b067073 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 13 Feb 2026 17:01:35 +0100 Subject: [PATCH 09/17] #2016 feat: show a warning when repeating a key code action less than 20 ms with expert mode triggers --- CHANGELOG.md | 1 + .../base/actions/ActionOptionsBottomSheet.kt | 26 +++++++++++++++++++ .../base/actions/ConfigActionsViewModel.kt | 12 ++++++++- base/src/main/res/values/strings.xml | 1 + 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc7665c05b..8a78684745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - #2024 support Expert mode on all Android versions supported by Key Mapper (8.0+). - #2025 add report bug button to home screen menu. - #2027 Make the key map sorting feature easier to understand. +- #2016 Show a warning when repeating a key code action less than 20 ms with expert mode triggers. ## Fixed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt index 90cf7cba0c..34fcde0e91 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionOptionsBottomSheet.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.HelpOutline +import androidx.compose.material.icons.rounded.Warning import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton @@ -126,6 +127,29 @@ fun ActionOptionsBottomSheet( ) } + if (state.showRepeatRateWarning) { + Spacer(Modifier.height(8.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(16.dp)) + Icon( + Icons.Rounded.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Spacer(Modifier.width(8.dp)) + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.action_repeat_rate_warning), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.labelLarge, + ) + Spacer(Modifier.width(16.dp)) + } + } + if (state.showRepeatRate) { Spacer(Modifier.height(8.dp)) @@ -421,6 +445,7 @@ private fun Preview() { showEditButton = true, showRepeat = true, isRepeatChecked = true, + showRepeatRateWarning = true, showRepeatRate = true, repeatRate = 400, @@ -480,6 +505,7 @@ private fun PreviewNoEditButton() { showEditButton = false, showRepeat = true, isRepeatChecked = true, + showRepeatRateWarning = true, showRepeatRate = true, repeatRate = 400, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index 80d3c6547f..877fb52385 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -11,6 +11,7 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.onboarding.OnboardingTipDelegate import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegate +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.isFixable import io.github.sds100.keymapper.base.utils.navigation.NavDestination @@ -29,7 +30,6 @@ import io.github.sds100.keymapper.common.utils.mapData import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission -import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class ConfigActionsViewModel @Inject constructor( @@ -261,6 +262,7 @@ class ConfigActionsViewModel @Inject constructor( ) RepeatMode.LIMIT_REACHED -> config.setActionStopRepeatingWhenLimitReached(uid) + RepeatMode.TRIGGER_PRESSED_AGAIN -> config.setActionStopRepeatingWhenTriggerPressedAgain(uid) } @@ -395,10 +397,17 @@ class ConfigActionsViewModel @Inject constructor( Int.MAX_VALUE } + val showRepeatRateWarning = + keyMap.isRepeatingActionsAllowed() && + action.data is ActionData.InputKeyEvent && + (action.repeatRate ?: defaultRepeatRate) < 20 && + keyMap.trigger.keys.any { it is EvdevTriggerKey } + return ActionOptionsState( showEditButton = action.data.isEditable(), showRepeat = keyMap.isRepeatingActionsAllowed(), + showRepeatRateWarning = showRepeatRateWarning, isRepeatChecked = action.repeat, showRepeatRate = keyMap.isChangingActionRepeatRateAllowed(action), @@ -470,6 +479,7 @@ data class ActionOptionsState( val isRepeatChecked: Boolean, val showRepeatRate: Boolean, + val showRepeatRateWarning: Boolean, val repeatRate: Int, val defaultRepeatRate: Int, diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 534906a56b..2c83a5b1a5 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1615,6 +1615,7 @@ Yes, delete Cancel Run these actions when you trigger the key map: + Repeating under 20 ms can lag! Adjust for your device and game. Choose side Brightness From 20f587a5823164653000de2865f1a0b35aa292b4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 13 Feb 2026 17:08:21 +0100 Subject: [PATCH 10/17] fix tests --- .../sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt index c1e2cfc7eb..f3f233625b 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt @@ -4,7 +4,6 @@ import android.view.KeyEvent import io.github.sds100.keymapper.base.repositories.FakePreferenceRepository import io.github.sds100.keymapper.base.system.inputmethod.FakeInputMethodAdapter import io.github.sds100.keymapper.base.utils.TestBuildConfigProvider -import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager @@ -77,7 +76,7 @@ class GetActionErrorUseCaseTest { cameraAdapter = mock(), soundsManager = mock(), ringtoneAdapter = mock(), - buildConfigProvider = TestBuildConfigProvider(sdkInt = Constants.SYSTEM_BRIDGE_MIN_API), + buildConfigProvider = TestBuildConfigProvider(sdkInt = 36), systemBridgeConnectionManager = mockSystemBridgeConnectionManager, preferenceRepository = fakePreferenceRepository, ) From 3621b8fa68f181891015ea41ce63fbaf9b1578a5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 13 Feb 2026 23:13:59 +0100 Subject: [PATCH 11/17] #2028 fix: work around Shizuku bug on Mediatek devices that prevents Expert mode from starting --- CHANGELOG.md | 1 + .../sysbridge/starter/SystemBridgeStarter.kt | 185 +++++++++++++----- 2 files changed, 140 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a78684745..820bb525f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ## Fixed - #2030 do not filter out unknown evdev key events. +- #2028 work around Shizuku bug on Mediatek devices that prevents Expert mode from starting ## [4.0.3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.3) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index 0d5429a851..f1b15f0f7c 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -34,11 +34,13 @@ import java.util.zip.ZipEntry import java.util.zip.ZipFile import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import rikka.shizuku.Shizuku import timber.log.Timber @@ -63,69 +65,160 @@ class SystemBridgeStarter @Inject constructor( private val startMutex: Mutex = Mutex() - private val shizukuStarterConnection: ServiceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { - Timber.i("Shizuku starter service connected") - - val service = IShizukuStarterService.Stub.asInterface(binder) - - Timber.i("Starting System Bridge with Shizuku starter service") - try { - runBlocking { - startSystemBridgeWithLock( - commandExecutor = { command -> - val output = service.executeCommand(command) - - if (output == null) { - KMError.UnknownIOError - } else { - Success(output) - } - }, - ) - } - } catch (e: RemoteException) { - Timber.e("Exception starting with Shizuku starter service: $e") - } finally { - try { - service.destroy() - } catch (_: DeadObjectException) { - // Do nothing. Service is already dead. - } - } - } + private companion object { + /** + * How long to wait for the Shizuku user service to connect before + * assuming it failed (e.g. due to the OEM bug on Xiaomi/MediaTek devices). + */ + private const val SHIZUKU_USER_SERVICE_TIMEOUT_MS = 5000L + } - override fun onServiceDisconnected(name: ComponentName?) { - // Do nothing. The service is supposed to immediately kill itself - // after starting the command. - } + private fun buildShizukuUserServiceArgs(): Shizuku.UserServiceArgs { + val serviceComponentName = ComponentName(ctx, ShizukuStarterService::class.java) + return Shizuku.UserServiceArgs(serviceComponentName) + .daemon(false) + .processNameSuffix("service") + .debuggable(BuildConfig.DEBUG) + .version(buildConfigProvider.versionCode) } - fun startWithShizuku() { + suspend fun startWithShizuku() { if (!Shizuku.pingBinder()) { Timber.w("Shizuku is not running. Cannot start System Bridge with Shizuku.") return } + val serviceConnected = CompletableDeferred() + // Shizuku will start a service which will then start the System Bridge. Shizuku won't be // used to start the System Bridge directly because native libraries need to be used // and we want to limit the dependency on Shizuku as much as possible. Also, the System // Bridge should still be running even if Shizuku dies. - val serviceComponentName = ComponentName(ctx, ShizukuStarterService::class.java) - val args = Shizuku.UserServiceArgs(serviceComponentName) - .daemon(false) - .processNameSuffix("service") - .debuggable(BuildConfig.DEBUG) - .version(buildConfigProvider.versionCode) + val connection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + Timber.i("Shizuku starter service connected") + + // Signal that the user service started successfully before doing the work. + serviceConnected.complete(Unit) + + val service = IShizukuStarterService.Stub.asInterface(binder) + + Timber.i("Starting System Bridge with Shizuku starter service") + try { + runBlocking { + startSystemBridgeWithLock( + commandExecutor = { command -> + val output = service.executeCommand(command) + + if (output == null) { + KMError.UnknownIOError + } else { + Success(output) + } + }, + ) + } + } catch (e: RemoteException) { + Timber.e("Exception starting with Shizuku starter service: $e") + } finally { + try { + service.destroy() + } catch (_: DeadObjectException) { + // Do nothing. Service is already dead. + } + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + // Do nothing. The service is supposed to immediately kill itself + // after starting the command. + } + } + + val args = buildShizukuUserServiceArgs() try { - Shizuku.bindUserService( - args, - shizukuStarterConnection, - ) + Shizuku.bindUserService(args, connection) } catch (e: Exception) { Timber.e("Exception when starting System Bridge with Shizuku. $e") + return + } + + // Wait for the user service to connect. On most devices this is near-instant. + // On Xiaomi/MediaTek devices with OEM-modified LoadedApk.makeApplicationInner(), + // the user service process crashes with a NPE before it can connect + // (see https://github.com/RikkaApps/Shizuku/issues/1198). + val connected = withTimeoutOrNull(SHIZUKU_USER_SERVICE_TIMEOUT_MS) { + serviceConnected.await() + } + + if (connected != null) { + // User service connected successfully; the existing flow handles the rest. + return + } + + // Timeout expired. Use peekUserService to confirm the service isn't running. + val serviceVersion = Shizuku.peekUserService(args, connection) + + if (serviceVersion >= 0) { + // The service is running but just slow to connect. Let it proceed. + Timber.w( + "Shizuku user service is running (version=$serviceVersion) but took " + + "longer than ${SHIZUKU_USER_SERVICE_TIMEOUT_MS}ms to connect.", + ) + return } + + // The service failed to start. Fall back to Shizuku.newProcess() via reflection. + Timber.w("Falling back to Shizuku.newProcess() workaround.") + startWithShizukuNewProcess() + } + + /** + * Fallback for starting the System Bridge via Shizuku when the user service fails to start. + * This can happen on Xiaomi/MediaTek devices where an OEM modification to + * LoadedApk.makeApplicationInner() causes a NPE during user service process creation. + * + * This method uses reflection to call the private Shizuku.newProcess() method, which + * spawns a process directly on the Shizuku server side without going through + * LoadedApk.makeApplication(), bypassing the OEM bug entirely. + * + * Note: Shizuku.newProcess() is deprecated and planned for removal in Shizuku API 14. + * This workaround should be removed once the upstream Shizuku bug is fixed. + */ + @Suppress("DiscouragedPrivateApi") + private suspend fun startWithShizukuNewProcess() { + Timber.i("Starting System Bridge with Shizuku newProcess fallback") + + startSystemBridgeWithLock( + commandExecutor = { command -> + try { + val method = Shizuku::class.java.getDeclaredMethod( + "newProcess", + Array::class.java, + Array::class.java, + String::class.java, + ) + method.isAccessible = true + + val process = method.invoke( + null, + arrayOf("sh", "-c", command), + null, + null, + ) as Process + + val stdout = process.inputStream.bufferedReader().readText() + val stderr = process.errorStream.bufferedReader().readText() + process.waitFor() + + Success("$stdout\n$stderr") + } catch (e: Exception) { + Timber.e("Shizuku newProcess fallback failed: $e") + KMError.Exception(e) + } + }, + ) } @RequiresApi(Build.VERSION_CODES.R) From 9493130c0bca1a7a80b1b95858528fa76b26b207 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 13 Feb 2026 23:14:36 +0100 Subject: [PATCH 12/17] feat: Show dialog if Expert mode fails to start after 60 seconds instead of waiting indefinitely --- CHANGELOG.md | 1 + .../base/actions/ConfigActionsViewModel.kt | 2 +- .../base/expertmode/ExpertModeScreen.kt | 40 ++++++++++++ .../base/expertmode/ExpertModeViewModel.kt | 15 +++++ .../expertmode/SystemBridgeSetupUseCase.kt | 4 ++ base/src/main/res/values/strings.xml | 2 + .../manager/SystemBridgeConnectionManager.kt | 6 +- .../service/SystemBridgeSetupController.kt | 64 +++++++++---------- 8 files changed, 95 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 820bb525f6..788da0afd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - #2025 add report bug button to home screen menu. - #2027 Make the key map sorting feature easier to understand. - #2016 Show a warning when repeating a key code action less than 20 ms with expert mode triggers. +- Show dialog if Expert mode fails to start after 60 seconds instead of waiting indefinitely. ## Fixed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index 877fb52385..c2fc69a8b3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -30,6 +30,7 @@ import io.github.sds100.keymapper.common.utils.mapData import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -42,7 +43,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class ConfigActionsViewModel @Inject constructor( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt index bf25890a53..290ccb07ec 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.material.icons.rounded.Numbers import androidx.compose.material.icons.rounded.RestartAlt import androidx.compose.material.icons.rounded.Usb import androidx.compose.material.icons.rounded.WarningAmber +import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CardDefaults @@ -87,6 +88,12 @@ fun ExpertModeScreen(modifier: Modifier = Modifier, viewModel: ExpertModeViewMod val expertModeWarningState by viewModel.warningState.collectAsStateWithLifecycle() val expertModeState by viewModel.state.collectAsStateWithLifecycle() + if (viewModel.showStartErrorDialog) { + SystemBridgeStartErrorDialog( + onDismissRequest = viewModel::dismissStartErrorDialog, + ) + } + ExpertModeScreen( modifier = modifier, onBackClick = viewModel::onBackClick, @@ -1074,6 +1081,39 @@ private fun PreviewUsbDebuggingSecuritySettingsCard() { } } +@Composable +private fun SystemBridgeStartErrorDialog( + modifier: Modifier = Modifier, + onDismissRequest: () -> Unit, +) { + AlertDialog( + modifier = modifier, + onDismissRequest = onDismissRequest, + title = { + Text(stringResource(R.string.expert_mode_start_error_dialog_title)) + }, + text = { + Text( + stringResource(R.string.expert_mode_start_error_dialog_message), + style = MaterialTheme.typography.bodyMedium, + ) + }, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.pos_ok)) + } + }, + ) +} + +@Preview +@Composable +private fun PreviewSystemBridgeStartErrorDialog() { + KeyMapperTheme { + SystemBridgeStartErrorDialog(onDismissRequest = {}) + } +} + @Preview @Composable private fun PreviewShellStartCard() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt index d9ce09e193..06becac982 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt @@ -70,6 +70,21 @@ class ExpertModeViewModel @Inject constructor( } }.stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + var showStartErrorDialog by mutableStateOf(false) + private set + + init { + viewModelScope.launch { + useCase.showStartError.collect { + showStartErrorDialog = true + } + } + } + + fun dismissStartErrorDialog() { + showStartErrorDialog = false + } + var showInfoCard by mutableStateOf(!useCase.isInfoDismissed()) private set diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt index 574c10f329..4cba168774 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt @@ -107,6 +107,9 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( override val isSystemBridgeStarting: Flow = systemBridgeSetupController.isStarting + override val showStartError: Flow = + systemBridgeSetupController.showStartError + override val isNotificationPermissionGranted: Flow = permissionAdapter.isGrantedFlow(Permission.POST_NOTIFICATIONS) @@ -325,6 +328,7 @@ interface SystemBridgeSetupUseCase { val isSystemBridgeConnected: Flow val isSystemBridgeStarting: Flow + val showStartError: Flow val nextSetupStep: Flow val isRootGranted: Flow diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 2c83a5b1a5..9bb7dc5d2f 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1693,6 +1693,8 @@ Get command Retry Failed to get command. Please try again. + Failed to start Expert Mode + The system bridge could not be started. Please try again or contact the developer at contact@keymapper.app if the problem persists. System Bridge Start Command Copy command to clipboard diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 9e92af19f8..3aaa0cbb74 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -99,7 +99,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( try { starter.refreshStarterScript() } catch (e: Exception) { - Timber.e("Failed to refresh system bridge starter script", e) + Timber.e("Failed to refresh system bridge starter script. $e") } } } @@ -282,7 +282,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( starter.startWithRoot() } - override fun startWithShizuku() { + override suspend fun startWithShizuku() { starter.startWithShizuku() } @@ -304,7 +304,7 @@ interface SystemBridgeConnectionManager { suspend fun startWithRoot() - fun startWithShizuku() + suspend fun startWithShizuku() suspend fun startWithAdb() diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 337b20cba6..608db926d8 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -23,6 +23,7 @@ import io.github.sds100.keymapper.sysbridge.adb.AdbManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import io.github.sds100.keymapper.sysbridge.manager.awaitConnected +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl.Companion.START_TIMEOUT_MS import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope @@ -54,6 +55,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( companion object { private const val DEVELOPER_OPTIONS_SETTING = "development_settings_enabled" private const val ADB_WIRELESS_SETTING = "adb_wifi_enabled" + private const val START_TIMEOUT_MS = 60_000L } private val activityManager: ActivityManager by lazy { ctx.getSystemService()!! } @@ -62,6 +64,9 @@ class SystemBridgeSetupControllerImpl @Inject constructor( override val isStarting: StateFlow = _isStarting private var startJob: Job? = null + private val _showStartError: MutableSharedFlow = MutableSharedFlow() + override val showStartError: Flow = _showStartError + override val isDeveloperOptionsEnabled: MutableStateFlow = MutableStateFlow(getDeveloperOptionsEnabled()) @@ -107,48 +112,30 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } override fun startWithRoot() { - if (startJob?.isActive == true) { - Timber.i("System Bridge is already starting") - return - } - - startJob = coroutineScope.launch { - _isStarting.value = true - try { - connectionManager.startWithRoot() - // Wait for the service to bind and start system bridge - withTimeoutOrNull(10000L) { - connectionManager.awaitConnected() - } - } finally { - _isStarting.value = false - } + launchStartJob { + connectionManager.startWithRoot() } } override fun startWithShizuku() { - if (startJob?.isActive == true) { - Timber.i("System Bridge is already starting") - return - } - - startJob = coroutineScope.launch { - _isStarting.value = true - try { - connectionManager.startWithShizuku() - - // Wait for the service to bind and start system bridge - withTimeoutOrNull(10000L) { - connectionManager.awaitConnected() - } - } finally { - _isStarting.value = false - } + launchStartJob { + connectionManager.startWithShizuku() } } @RequiresApi(Build.VERSION_CODES.R) override fun startWithAdb() { + launchStartJob { + connectionManager.startWithAdb() + } + } + + /** + * Launch a start job that runs [start], then waits up to [START_TIMEOUT_MS] for the + * system bridge to connect. If it does not connect in time, an error is emitted + * via [showStartError]. + */ + private fun launchStartJob(start: suspend () -> Unit) { if (startJob?.isActive == true) { Timber.i("System Bridge is already starting") return @@ -157,11 +144,17 @@ class SystemBridgeSetupControllerImpl @Inject constructor( startJob = coroutineScope.launch { _isStarting.value = true try { - connectionManager.startWithAdb() + start() + // Wait for the service to bind and start system bridge - withTimeoutOrNull(10000L) { + val connected = withTimeoutOrNull(START_TIMEOUT_MS) { connectionManager.awaitConnected() } + + if (connected == null) { + Timber.e("System Bridge failed to start within ${START_TIMEOUT_MS}ms") + _showStartError.emit(Unit) + } } finally { _isStarting.value = false } @@ -422,6 +415,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( interface SystemBridgeSetupController { val setupAssistantStep: Flow val isStarting: StateFlow + val showStartError: Flow val isDeveloperOptionsEnabled: Flow fun enableDeveloperOptions() From c8851a3ce9774f72b6cfc48159ab279a1700369f Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 14 Feb 2026 12:40:10 +0100 Subject: [PATCH 13/17] #2034 catch errors when injecting events with Expert mode on Xiaomi devices and show warning to fix on home screen --- CHANGELOG.md | 3 +- .../base/expertmode/ExpertModeScreen.kt | 33 ++--- .../base/expertmode/ExpertModeViewModel.kt | 6 +- .../expertmode/SystemBridgeSetupUseCase.kt | 6 +- .../base/home/KeyMapListViewModel.kt | 122 ++++++++++-------- .../base/home/ShowHomeScreenAlertsUseCase.kt | 24 +++- .../sysbridge/service/SystemBridge.kt | 7 +- .../service/SystemBridgeSetupController.kt | 10 +- 8 files changed, 129 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 788da0afd3..848f7cc32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ ## Fixed - #2030 do not filter out unknown evdev key events. -- #2028 work around Shizuku bug on Mediatek devices that prevents Expert mode from starting +- #2028 work around Shizuku bug on Mediatek devices that prevents Expert mode from starting. +- #2034 catch errors when injecting events with Expert mode on Xiaomi devices and show warning to fix on home screen. ## [4.0.3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.3) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt index 290ccb07ec..42fe615f04 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt @@ -318,15 +318,6 @@ private fun LoadedContent( when (state) { is ExpertModeState.Started -> { - ExpertModeStartedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - onStopClick = onStopServiceClick, - ) - - Spacer(modifier = Modifier.height(8.dp)) - if (!state.isDefaultUsbModeCompatible) { IncompatibleUsbModeCard( modifier = Modifier @@ -344,12 +335,20 @@ private fun LoadedContent( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), - onLaunchDeveloperOptionsClick = onLaunchDeveloperOptionsClick, ) Spacer(modifier = Modifier.height(8.dp)) } + ExpertModeStartedCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + onStopClick = onStopServiceClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + SwitchPreferenceCompose( modifier = Modifier.padding(horizontal = 8.dp), title = stringResource(R.string.title_pref_expert_mode_auto_start), @@ -530,10 +529,8 @@ private fun IncompatibleUsbModeCard(modifier: Modifier = Modifier) { } @Composable -private fun UsbDebuggingSecuritySettingsCard( - modifier: Modifier = Modifier, - onLaunchDeveloperOptionsClick: () -> Unit = {}, -) { +private fun UsbDebuggingSecuritySettingsCard(modifier: Modifier = Modifier) { + val ctx = LocalContext.current SetupCard( modifier = modifier, color = MaterialTheme.colorScheme.errorContainer, @@ -558,7 +555,13 @@ private fun UsbDebuggingSecuritySettingsCard( buttonText = stringResource( R.string.expert_mode_usb_debugging_security_settings_button, ), - onButtonClick = onLaunchDeveloperOptionsClick, + onButtonClick = { + SettingsUtils.launchSettingsScreen( + ctx, + Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS, + null, + ) + }, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt index 06becac982..720296c005 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt @@ -213,15 +213,15 @@ class ExpertModeViewModel @Inject constructor( private fun startedStateFlow(): Flow = combine( useCase.isAutoStartBootEnabled, useCase.isAutoStartBootAllowed, - useCase.shellHasGrantRuntimePermissions, - ) { autoStartBootChecked, autoStartBootEnabled, shellHasGrantRuntimePermissions -> + useCase.xiaomiAdbSecuritySettingsEnabled, + ) { autoStartBootChecked, autoStartBootEnabled, xiaomiAdbSecuritySettingsEnabled -> ExpertModeState.Started( isDefaultUsbModeCompatible = useCase.isCompatibleUsbModeSelected().valueOrNull() ?: false, autoStartBootChecked = autoStartBootChecked, autoStartBootEnabled = autoStartBootEnabled, - showXiaomiAdbInputSecurityWarning = !shellHasGrantRuntimePermissions, + showXiaomiAdbInputSecurityWarning = !xiaomiAdbSecuritySettingsEnabled, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt index 4cba168774..f72efe4f95 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt @@ -187,8 +187,8 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( systemBridgeSetupController.launchDeveloperOptions() } - override val shellHasGrantRuntimePermissions: Flow = - systemBridgeSetupController.shellHasGrantRuntimePermissions + override val xiaomiAdbSecuritySettingsEnabled: Flow = + systemBridgeSetupController.xiaomiAdbSecuritySettingsEnabled override fun connectWifiNetwork() { networkAdapter.connectWifiNetwork() @@ -352,7 +352,7 @@ interface SystemBridgeSetupUseCase { fun startSystemBridgeWithAdb() fun autoStartSystemBridgeWithAdb() - val shellHasGrantRuntimePermissions: Flow + val xiaomiAdbSecuritySettingsEnabled: Flow fun isCompatibleUsbModeSelected(): KMResult diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt index 4e85c46321..414dfc2ac2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt @@ -66,6 +66,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -99,6 +100,7 @@ class KeyMapListViewModel( const val ID_BATTERY_OPTIMISATION_LIST_ITEM = "battery_optimised" const val ID_LOGGING_ENABLED_LIST_ITEM = "logging_enabled" const val ID_NOTIFICATION_PERMISSION_DENIED_LIST_ITEM = "notification_permission_denied" + const val ID_XIAOMI_ADB_SECURITY_SETTINGS_LIST_ITEM = "xiaomi_adb_security_settings" } val sortViewModel = SortViewModel(coroutineScope, sortKeyMaps) @@ -144,70 +146,83 @@ class KeyMapListViewModel( private val isEditingGroupName = MutableStateFlow(false) - private val warnings: Flow> = combine( - showAlertsUseCase.isBatteryOptimised, - accessibilityServiceState, - showAlertsUseCase.hideAlerts, - showAlertsUseCase.isLoggingEnabled, - showAlertsUseCase.showNotificationPermissionAlert, - ) { - isBatteryOptimised, - serviceState, - isHidden, - isLoggingEnabled, - showNotificationPermissionAlert, - -> - if (isHidden) { - return@combine emptyList() - } - - buildList { - when (serviceState) { - AccessibilityServiceState.CRASHED -> - add( - HomeWarningListItem( - ID_ACCESSIBILITY_SERVICE_CRASHED_LIST_ITEM, - getString(R.string.home_error_accessibility_service_is_crashed), - ), - ) - - AccessibilityServiceState.DISABLED -> - add( - HomeWarningListItem( - ID_ACCESSIBILITY_SERVICE_DISABLED_LIST_ITEM, - getString(R.string.home_error_accessibility_service_is_disabled), - ), - ) - - AccessibilityServiceState.ENABLED -> {} + private val warnings: Flow> = + showAlertsUseCase.hideAlerts.flatMapLatest { hideAlerts -> + if (hideAlerts) { + flowOf(emptyList()) + } else { + combine( + showAlertsUseCase.isBatteryOptimised, + accessibilityServiceState, + showAlertsUseCase.isLoggingEnabled, + showAlertsUseCase.showNotificationPermissionAlert, + showAlertsUseCase.showXiaomiAdbSecuritySettingsWarning, + this::buildWarningListItems, + ) } + } - if (isBatteryOptimised) { + private fun buildWarningListItems( + isBatteryOptimised: Boolean, + serviceState: AccessibilityServiceState, + isLoggingEnabled: Boolean, + showNotificationPermissionAlert: Boolean, + showXiaomiAdbSecuritySettingsWarning: Boolean, + ): List = buildList { + when (serviceState) { + AccessibilityServiceState.CRASHED -> add( HomeWarningListItem( - ID_BATTERY_OPTIMISATION_LIST_ITEM, - getString(R.string.home_error_is_battery_optimised), + ID_ACCESSIBILITY_SERVICE_CRASHED_LIST_ITEM, + getString(R.string.home_error_accessibility_service_is_crashed), ), ) - } // don't show a success message for this - if (showNotificationPermissionAlert) { + AccessibilityServiceState.DISABLED -> add( HomeWarningListItem( - ID_NOTIFICATION_PERMISSION_DENIED_LIST_ITEM, - getString(R.string.home_error_notification_permission), + ID_ACCESSIBILITY_SERVICE_DISABLED_LIST_ITEM, + getString(R.string.home_error_accessibility_service_is_disabled), ), ) - } - if (isLoggingEnabled) { - add( - HomeWarningListItem( - ID_LOGGING_ENABLED_LIST_ITEM, - getString(R.string.home_error_logging_enabled), - ), - ) - } + AccessibilityServiceState.ENABLED -> {} + } + + if (isBatteryOptimised) { + add( + HomeWarningListItem( + ID_BATTERY_OPTIMISATION_LIST_ITEM, + getString(R.string.home_error_is_battery_optimised), + ), + ) + } // don't show a success message for this + + if (showNotificationPermissionAlert) { + add( + HomeWarningListItem( + ID_NOTIFICATION_PERMISSION_DENIED_LIST_ITEM, + getString(R.string.home_error_notification_permission), + ), + ) + } + + if (showXiaomiAdbSecuritySettingsWarning) { + add( + HomeWarningListItem( + ID_XIAOMI_ADB_SECURITY_SETTINGS_LIST_ITEM, + getString(R.string.expert_mode_usb_debugging_security_settings_description), + ), + ) + } + + if (isLoggingEnabled) { + add( + HomeWarningListItem( + ID_LOGGING_ENABLED_LIST_ITEM, + getString(R.string.home_error_logging_enabled), + ), + ) } } @@ -687,6 +702,9 @@ class KeyMapListViewModel( ID_NOTIFICATION_PERMISSION_DENIED_LIST_ITEM -> showNotificationPermissionAlertDialog() + + ID_XIAOMI_ADB_SECURITY_SETTINGS_LIST_ITEM -> + showAlertsUseCase.launchDeveloperOptions() } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/ShowHomeScreenAlertsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/ShowHomeScreenAlertsUseCase.kt index d37f979aef..40b9662f3f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/ShowHomeScreenAlertsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/ShowHomeScreenAlertsUseCase.kt @@ -3,7 +3,9 @@ package io.github.sds100.keymapper.base.home import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import javax.inject.Inject @@ -14,8 +16,9 @@ import kotlinx.coroutines.flow.map class ShowHomeScreenAlertsUseCaseImpl @Inject constructor( private val preferences: PreferenceRepository, private val permissions: PermissionAdapter, - private val accessibilityServiceAdapter: AccessibilityServiceAdapter, private val pauseKeyMapsUseCase: PauseKeyMapsUseCase, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val systemBridgeSetupController: SystemBridgeSetupController, ) : ShowHomeScreenAlertsUseCase { override val hideAlerts: Flow = preferences.get(Keys.hideHomeScreenAlerts).map { it == true } @@ -48,6 +51,19 @@ class ShowHomeScreenAlertsUseCaseImpl @Inject constructor( !isGranted && !neverShow } + override val showXiaomiAdbSecuritySettingsWarning: Flow = + combine( + systemBridgeConnectionManager.connectionState, + systemBridgeSetupController.xiaomiAdbSecuritySettingsEnabled, + ) { connectionState, xiaomiSettingEnabled -> + connectionState is SystemBridgeConnectionState.Connected && + !xiaomiSettingEnabled + } + + override fun launchDeveloperOptions() { + systemBridgeSetupController.launchDeveloperOptions() + } + override fun requestNotificationPermission() { permissions.request(Permission.POST_NOTIFICATIONS) } @@ -70,4 +86,8 @@ interface ShowHomeScreenAlertsUseCase { val showNotificationPermissionAlert: Flow fun requestNotificationPermission() fun neverShowNotificationPermissionAlert() + + val showXiaomiAdbSecuritySettingsWarning: Flow + + fun launchDeveloperOptions() } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 2aa3886f91..888958cc41 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -486,7 +486,12 @@ class SystemBridge : ISystemBridge.Stub() { } override fun injectInputEvent(event: InputEvent?, mode: Int): Boolean { - return inputManager.injectInputEvent(event, mode) + try { + return inputManager.injectInputEvent(event, mode) + } catch (e: SecurityException) { + logCallback?.onLog(Log.WARN, "Failed to inject event due to security exception: $e") + return false + } } override fun getEvdevInputDevices(): Array { diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 608db926d8..565c890de4 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -81,8 +81,8 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private val isAdbPairedResult: MutableStateFlow = MutableStateFlow(null) private var isAdbPairedJob: Job? = null - override val shellHasGrantRuntimePermissions: MutableStateFlow = - MutableStateFlow(getShellHasGrantRuntimePermissions()) + override val xiaomiAdbSecuritySettingsEnabled: MutableStateFlow = + MutableStateFlow(getXiaomiAdbSecuritySettingsEnabled()) init { // Automatically go back to the Key Mapper app when turning on wireless debugging @@ -365,7 +365,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( fun invalidateSettings() { isDeveloperOptionsEnabled.update { getDeveloperOptionsEnabled() } isWirelessDebuggingEnabled.update { getWirelessDebuggingEnabled() } - shellHasGrantRuntimePermissions.update { getShellHasGrantRuntimePermissions() } + xiaomiAdbSecuritySettingsEnabled.update { getXiaomiAdbSecuritySettingsEnabled() } } private fun getDeveloperOptionsEnabled(): Boolean { @@ -384,7 +384,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } - private fun getShellHasGrantRuntimePermissions(): Boolean { + private fun getXiaomiAdbSecuritySettingsEnabled(): Boolean { return ctx.packageManager.checkPermission( "android.permission.GRANT_RUNTIME_PERMISSIONS", "com.android.shell", @@ -444,7 +444,7 @@ interface SystemBridgeSetupController { * the Shell to have permission to grant runtime permissions, such as WRITE_SECURE_SETTINGS * to Key Mapper. */ - val shellHasGrantRuntimePermissions: StateFlow + val xiaomiAdbSecuritySettingsEnabled: StateFlow fun launchDeveloperOptions() From ac068e932fa3bf663f6da41a7857f752bd4bc927 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 14 Feb 2026 13:02:33 +0100 Subject: [PATCH 14/17] fix tests --- .../base/actions/ConfigShellCommandViewModelTest.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModelTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModelTest.kt index 558dfe2361..300b667a5a 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModelTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModelTest.kt @@ -10,8 +10,10 @@ import io.github.sds100.keymapper.common.models.ShellResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -68,7 +70,15 @@ class ConfigShellCommandViewModelTest { mockExecuteShellCommandUseCase = mock() mockNavigationProvider = mock() - mockSystemBridgeConnectionManager = mock() + mockSystemBridgeConnectionManager = mock { + on { connectionState }.thenReturn( + MutableStateFlow( + SystemBridgeConnectionState.Connected( + 0L, + ), + ), + ) + } fakePreferenceRepository = FakePreferenceRepository() From 0cd78f009b3d73861eb7ace0477b90f3cba8b414 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 14 Feb 2026 14:01:19 +0100 Subject: [PATCH 15/17] fix: use same NDK version as evdev module --- sysbridge/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts index a08999fc43..f73f322211 100644 --- a/sysbridge/build.gradle.kts +++ b/sysbridge/build.gradle.kts @@ -11,7 +11,7 @@ android { namespace = "io.github.sds100.keymapper.sysbridge" compileSdk = libs.versions.compile.sdk.get().toInt() - ndkVersion = "27.2.12479018" + ndkVersion = "27.3.13750724" defaultConfig { // System bridge originally only supported Android 10+ because that was the min sdk From 88d38d32f3192ee81d5f9720c1e07f43915d3126 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 20 Feb 2026 16:33:10 -0700 Subject: [PATCH 16/17] fix: do not show hold down duration for double press and sequence trigger key maps --- .../java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt index 4a211445ff..f75bbf55c6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt @@ -49,7 +49,7 @@ data class KeyMap( KeyMapAlgorithm.performActionOnDown(trigger) && action.data.canBeHeldDown() fun isHoldingDownActionBeforeRepeatingAllowed(action: Action): Boolean = - action.repeat && action.holdDown + action.repeat && action.holdDown && isHoldingDownActionAllowed(action) fun isChangingRepeatModeAllowed(action: Action): Boolean = action.repeat && isRepeatingActionsAllowed() @@ -58,7 +58,7 @@ data class KeyMap( action.repeat && isRepeatingActionsAllowed() fun isStopHoldingDownActionWhenTriggerPressedAgainAllowed(action: Action): Boolean = - action.holdDown && !action.repeat + action.holdDown && !action.repeat && isHoldingDownActionAllowed(action) fun isDelayBeforeNextActionAllowed(): Boolean = actionList.isNotEmpty() } From c4a11b05e3489b2a25ed3a14993b2ba64e7582b1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 21 Feb 2026 14:10:54 -0700 Subject: [PATCH 17/17] update changelog date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 848f7cc32d..ba2ce2ef00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [4.0.4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.4) -#### TO BE RELEASED +#### 21 February 2026 ## Added