diff --git a/CHANGELOG.md b/CHANGELOG.md index 93aff66fde..f41e8d4d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [4.0.1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.1) + +#### 01 February 2026 + +## Fixed +- #2007 Volume up/down action bottom sheet is cut off on some devices. +- #2004 Do not crash when launching wireless debugging screen on some devices. +- #2005 NPE in onSaveInstanceState. +- #2000 tell the user to tap OS version or build number. +- #1996 Check if Night Shift is supported on a device before activating to prevent the screen going black when activated. +- #1999 Use a more reliable method to check whether Shell has GRANT_RUNTIME_PERMISSIONS permission. + ## [4.0.0](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0) #### 25 January 2026 diff --git a/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt b/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt index 0a1fec850b..95b18b79c0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt @@ -60,7 +60,7 @@ class MainFragment : Fragment() { private lateinit var composeView: ComposeView - private lateinit var navController: NavHostController + private var navController: NavHostController? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -98,7 +98,7 @@ class MainFragment : Fragment() { // is destroyed setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - SetupNavigation(navigationProvider, navController) + SetupNavigation(navigationProvider, navController!!) KeyMapperTheme { BaseMainNavHost( @@ -111,10 +111,10 @@ class MainFragment : Fragment() { ), ), ), - navController = navController, + navController = navController!!, setupAccessibilityServiceDelegate = setupAccessibilityServiceDelegate, composableDestinations = { - composableDestinations(navController) + composableDestinations(navController!!) }, ) } @@ -123,7 +123,7 @@ class MainFragment : Fragment() { } override fun onSaveInstanceState(outState: Bundle) { - navController.saveState()?.let(outState::putAll) + navController?.saveState()?.let(outState::putAll) super.onSaveInstanceState(outState) } @@ -131,7 +131,7 @@ class MainFragment : Fragment() { override fun onDestroyView() { // onSaveInstanceState is only called when the activity's onSaveInstanceState method // is called so use our own place to save the navigation state - navigationProvider.savedState = navController.saveState() + navigationProvider.savedState = navController?.saveState() super.onDestroyView() } diff --git a/app/version.properties b/app/version.properties index c11699a2d3..a40000211d 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.0.0 -VERSION_CODE=238 +VERSION_NAME=4.0.1 +VERSION_CODE=241 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/IsActionSupportedUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/IsActionSupportedUseCase.kt index bb2b26ea5c..cea7c9a0af 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/IsActionSupportedUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/IsActionSupportedUseCase.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.base.actions import android.content.pm.PackageManager +import android.content.res.Resources import android.os.Build import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.system.SystemError @@ -56,6 +57,18 @@ class IsActionSupportedUseCaseImpl( } } + if (id == ActionId.TOGGLE_NIGHT_SHIFT || + id == ActionId.ENABLE_NIGHT_SHIFT || + id == ActionId.DISABLE_NIGHT_SHIFT + ) { + // See https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/core/java/android/hardware/display/ColorDisplayManager.java;l=498;drc=787314ed22d859e510163327dd6c58b215c2f7f9 + val res = Resources.getSystem() + val resId = res.getIdentifier("config_nightDisplayAvailable", "bool", "android") + if (resId == 0 || !res.getBoolean(resId)) { + return KMError.NightDisplayNotSupported + } + } + if (ActionUtils.getRequiredPermissions(id).contains(Permission.ROOT) && !permissionAdapter.isGranted(Permission.ROOT) ) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt index 10b8b0ead8..959951b03c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/VolumeActionBottomSheet.kt @@ -162,45 +162,45 @@ private fun VolumeActionBottomSheet( isChecked = state.showVolumeUi, onCheckedChange = onToggleShowVolumeUi, ) - } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedButton( - modifier = Modifier.weight(1f), - onClick = { - scope.launch { - sheetState.hide() - onDismissRequest() - } - }, + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { - Text(stringResource(R.string.neg_cancel)) - } + OutlinedButton( + modifier = Modifier.weight(1f), + onClick = { + scope.launch { + sheetState.hide() + onDismissRequest() + } + }, + ) { + Text(stringResource(R.string.neg_cancel)) + } - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(16.dp)) - Button( - modifier = Modifier.weight(1f), - onClick = onDoneClick, - ) { - Text(stringResource(R.string.pos_done)) + Button( + modifier = Modifier.weight(1f), + onClick = onDoneClick, + ) { + Text(stringResource(R.string.pos_done)) + } } - } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(16.dp)) + } } } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(heightDp = 400) @Composable private fun PreviewVolumeActionBottomSheet() { KeyMapperTheme { 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 8d75d6dbc0..5bd52a8319 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 @@ -332,7 +332,7 @@ private fun LoadedContent( // Only show auto-start options and warnings when Expert Mode is started // Show USB debugging security settings warning if disabled - if (state.isAdbInputSecurityEnabled == false) { + if (state.showXiaomiAdbInputSecurityWarning) { UsbDebuggingSecuritySettingsCard( modifier = Modifier .fillMaxWidth() @@ -970,7 +970,7 @@ private fun PreviewDark() { isDefaultUsbModeCompatible = true, autoStartBootChecked = true, autoStartBootEnabled = true, - isAdbInputSecurityEnabled = null, + showXiaomiAdbInputSecurityWarning = false, ), ), showInfoCard = false, @@ -1013,7 +1013,7 @@ private fun PreviewStarted() { isDefaultUsbModeCompatible = false, autoStartBootChecked = false, autoStartBootEnabled = true, - isAdbInputSecurityEnabled = null, + showXiaomiAdbInputSecurityWarning = false, ), ), showInfoCard = false, @@ -1062,7 +1062,7 @@ private fun PreviewUsbDebuggingSecuritySettingsCard() { isDefaultUsbModeCompatible = true, autoStartBootChecked = false, autoStartBootEnabled = true, - isAdbInputSecurityEnabled = false, + showXiaomiAdbInputSecurityWarning = false, ), ), showInfoCard = false, 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 04680bc924..d9ce09e193 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 @@ -198,15 +198,15 @@ class ExpertModeViewModel @Inject constructor( private fun startedStateFlow(): Flow = combine( useCase.isAutoStartBootEnabled, useCase.isAutoStartBootAllowed, - useCase.isAdbInputSecurityEnabled, - ) { autoStartBootChecked, autoStartBootEnabled, isAdbInputSecurityEnabled -> + useCase.shellHasGrantRuntimePermissions, + ) { autoStartBootChecked, autoStartBootEnabled, shellHasGrantRuntimePermissions -> ExpertModeState.Started( isDefaultUsbModeCompatible = useCase.isCompatibleUsbModeSelected().valueOrNull() ?: false, autoStartBootChecked = autoStartBootChecked, autoStartBootEnabled = autoStartBootEnabled, - isAdbInputSecurityEnabled = isAdbInputSecurityEnabled, + showXiaomiAdbInputSecurityWarning = !shellHasGrantRuntimePermissions, ) } } @@ -230,7 +230,7 @@ sealed class ExpertModeState { val isDefaultUsbModeCompatible: Boolean, val autoStartBootChecked: Boolean, val autoStartBootEnabled: Boolean, - val isAdbInputSecurityEnabled: Boolean?, + val showXiaomiAdbInputSecurityWarning: Boolean, ) : ExpertModeState() } 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 a35ca5f30d..7caf46f401 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 @@ -186,13 +186,8 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( systemBridgeSetupController.launchDeveloperOptions() } - @RequiresApi(Build.VERSION_CODES.R) - override val isAdbInputSecurityEnabled: Flow = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - systemBridgeSetupController.isAdbInputSecurityEnabled - } else { - flowOf(null) - } + override val shellHasGrantRuntimePermissions: Flow = + systemBridgeSetupController.shellHasGrantRuntimePermissions override fun connectWifiNetwork() { networkAdapter.connectWifiNetwork() @@ -355,7 +350,7 @@ interface SystemBridgeSetupUseCase { fun startSystemBridgeWithAdb() fun autoStartSystemBridgeWithAdb() - val isAdbInputSecurityEnabled: Flow + val shellHasGrantRuntimePermissions: Flow fun isCompatibleUsbModeSelected(): KMResult diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt index a791092b9a..06cf713a1f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt @@ -362,6 +362,9 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { R.string.error_variable_flashlight_strength_unsupported, ) + KMError.NightDisplayNotSupported -> + resourceProvider.getString(R.string.error_night_display_not_supported) + is KMError.FailedToModifySystemSetting -> resourceProvider.getString( R.string.error_failed_to_modify_system_setting, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SearchAppBarActions.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SearchAppBarActions.kt index 02176b229e..d8e9c4d04c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SearchAppBarActions.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SearchAppBarActions.kt @@ -46,10 +46,9 @@ fun RowScope.SearchAppBarActions( } DockedSearchBar( - modifier = Modifier.Companion.align(Alignment.Companion.CenterVertically), + modifier = Modifier.align(Alignment.CenterVertically), inputField = { SearchBarDefaults.InputField( - modifier = Modifier.Companion.align(Alignment.Companion.CenterVertically), onSearch = { onQueryChange(it) isExpanded = false diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 0a2ddf7718..310a1c2da6 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -817,6 +817,7 @@ No front flash No back flash Variable flashlight strength unsupported + Your device doesn\'t support night display. Accessibility service needs to be enabled! The accessibility service needs to be restarted! @@ -1754,7 +1755,7 @@ Finish Enable developer options - Tap build number repeatedly + Tap build number or OS version repeatedly Pairing automatically Searching for pairing code and port… diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt index 0e9f9f9f52..bce1fbb41a 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/KMResult.kt @@ -53,6 +53,7 @@ abstract class KMError : KMResult() { data object MaxCamerasInUse : KMError() data object CameraError : KMError() data object CameraVariableFlashlightStrengthUnsupported : KMError() + data object NightDisplayNotSupported : KMError() data class FailedToModifySystemSetting(val setting: String) : KMError() data object SwitchImeFailed : KMError() diff --git a/evdev/.idea/gradle.xml b/evdev/.idea/gradle.xml index e77e5a3224..cb0c8464b9 100644 --- a/evdev/.idea/gradle.xml +++ b/evdev/.idea/gradle.xml @@ -6,6 +6,20 @@ diff --git a/evdev/src/main/rust/evdev_manager/jni/src/evdev_jni_observer.rs b/evdev/src/main/rust/evdev_manager/jni/src/evdev_jni_observer.rs index 5bf31d541f..738f115bc0 100644 --- a/evdev/src/main/rust/evdev_manager/jni/src/evdev_jni_observer.rs +++ b/evdev/src/main/rust/evdev_manager/jni/src/evdev_jni_observer.rs @@ -59,6 +59,8 @@ impl EvdevJniObserver { // Button up - check if held for 10+ seconds let down_time = *time_guard; if down_time > 0 && time_sec - down_time >= 10 { + // Must send log to Key Mapper for diagnostic purposes. + warn!("Emergency killing system bridge!"); // Call BaseSystemBridge.onEmergencyKillSystemBridge() via JNI if let Ok(mut env) = self.jvm.attach_current_thread() { let _ = env.call_method( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8caa284e77..784eb823c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ androidx-viewpager2 = "1.1.0" dagger-hilt-android = "2.56.2" hilt-navigation-compose = "1.3.0" -compose-bom = "2025.11.00" +compose-bom = "2026.01.01" compose-compiler = "1.5.10" # kotlinCompilerExtensionVersion desugar-jdk-libs = "2.1.5" @@ -56,7 +56,7 @@ ksp-gradle-plugin = "2.1.0-1.0.28" ktlint-gradle = "13.1.0" #leakcanary = "2.6" # Commented out in original file lingala-zip4j = "2.8.0" -material = "1.13.0-alpha13" +material = "1.13.0" mflisar-dragselectrecyclerview = "0.3" mockito-android = "4.6.1" 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 a6ba627e9b..545e46f2a6 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 @@ -19,15 +19,12 @@ 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.Success import io.github.sds100.keymapper.common.utils.isSuccess import io.github.sds100.keymapper.common.utils.onSuccess 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.SystemBridgeConnectionState.Connected import io.github.sds100.keymapper.sysbridge.manager.awaitConnected -import io.github.sds100.keymapper.sysbridge.manager.isConnected import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope @@ -81,8 +78,8 @@ class SystemBridgeSetupControllerImpl @Inject constructor( private val isAdbPairedResult: MutableStateFlow = MutableStateFlow(null) private var isAdbPairedJob: Job? = null - override val isAdbInputSecurityEnabled: MutableStateFlow = MutableStateFlow(null) - private var checkAdbInputSecurityJob: Job? = null + override val shellHasGrantRuntimePermissions: MutableStateFlow = + MutableStateFlow(getShellHasGrantRuntimePermissions()) init { // Automatically go back to the Key Mapper app when turning on wireless debugging @@ -94,7 +91,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // Do not automatically go back to Key Mapper after this step because // some devices show a dialog that will be auto dismissed resulting in wireless // ADB being immediately disabled. E.g OnePlus 6T Oxygen OS 11 - // Note: ADB input security check is handled by monitoring isWirelessDebuggingEnabled flow } } @@ -110,27 +106,6 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } } - - // Automatically check ADB input security when SystemBridge is connected - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - coroutineScope.launch { - // Check when SystemBridge becomes connected - connectionManager.connectionState.collect { connectionState -> - when (connectionState) { - is Connected -> { - // Delay a bit to ensure SystemBridge is ready - kotlinx.coroutines.delay(1000L) - checkAdbInputSecurityEnabled() - } - - is SystemBridgeConnectionState.Disconnected -> { - // Reset to null when SystemBridge is disconnected - isAdbInputSecurityEnabled.value = null - } - } - } - } - } } override fun startWithRoot() { @@ -369,6 +344,10 @@ class SystemBridgeSetupControllerImpl @Inject constructor( try { ctx.startActivity(quickSettingsIntent) return true + } catch (_: SecurityException) { + highlightDeveloperOptionsWirelessDebuggingOption() + + return false } catch (_: ActivityNotFoundException) { highlightDeveloperOptionsWirelessDebuggingOption() @@ -392,51 +371,10 @@ class SystemBridgeSetupControllerImpl @Inject constructor( ) } - private fun checkAdbInputSecurityEnabled() { - if (!connectionManager.isConnected()) { - isAdbInputSecurityEnabled.value = null - return - } - - // Only run one check at a time - if (checkAdbInputSecurityJob == null || checkAdbInputSecurityJob?.isCompleted == true) { - checkAdbInputSecurityJob?.cancel() - - checkAdbInputSecurityJob = coroutineScope.launch { - try { - val result = connectionManager.run { systemBridge -> - systemBridge.executeCommand("getprop persist.security.adbinput", 5000L) - } - - val isEnabled = when (result) { - is Success -> { - val stdout = result.value.stdout.trim() - - when (stdout) { - "1" -> true - - "0" -> false - - // If it is empty or anything else then set the value to null - // because what we are expecting does not exist. - else -> null - } - } - - else -> null - } - isAdbInputSecurityEnabled.value = isEnabled - } catch (_: Exception) { - // If check fails, set to null - isAdbInputSecurityEnabled.value = null - } - } - } - } - fun invalidateSettings() { isDeveloperOptionsEnabled.update { getDeveloperOptionsEnabled() } isWirelessDebuggingEnabled.update { getWirelessDebuggingEnabled() } + shellHasGrantRuntimePermissions.update { getShellHasGrantRuntimePermissions() } } private fun getDeveloperOptionsEnabled(): Boolean { @@ -455,6 +393,13 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } + private fun getShellHasGrantRuntimePermissions(): Boolean { + return ctx.packageManager.checkPermission( + "android.permission.GRANT_RUNTIME_PERMISSIONS", + "com.android.shell", + ) == PackageManager.PERMISSION_GRANTED + } + private fun getKeyMapperAppTask(): ActivityManager.AppTask? { val task = activityManager.appTasks ?.firstOrNull { @@ -502,10 +447,14 @@ interface SystemBridgeSetupController { fun autoStartWithAdb() /** - * If this value is null then the option does not exist or can not be checked - * because the system bridge is disconnected. + * See issue #1965. + * + * Whether the Shell package has GRANT_RUNTIME_PERMISSIONS permission. On Xiaomi devices, the + * user needs to enable "USB debugging security settings" in Developer Options for + * the Shell to have permission to grant runtime permissions, such as WRITE_SECURE_SETTINGS + * to Key Mapper. */ - val isAdbInputSecurityEnabled: StateFlow + val shellHasGrantRuntimePermissions: StateFlow fun launchDeveloperOptions()