diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2ce2ef00..e66c03232b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [4.0.5](https://github.com/sds100/KeyMapper/releases/tag/v4.0.5) + +#### 26 February 2026 + +## Fixed + +- #2047 allow empty text in Text action. +- #2056 replace old "PRO" in triggers on home screen with "Expert". +- #2053 reduce latency when a lot of key maps with open app actions. +- #2054 fix "Fix key event action" bottom sheet done button being hidden on small screens. + ## [4.0.4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.4) #### 21 February 2026 diff --git a/app/version.properties b/app/version.properties index bb6898e9f9..c8c849f38d 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.0.4 -VERSION_CODE=246 +VERSION_NAME=4.0.5 +VERSION_CODE=247 diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml index faddfc687d..c4a4aea9ea 100644 --- a/base/src/main/AndroidManifest.xml +++ b/base/src/main/AndroidManifest.xml @@ -5,7 +5,8 @@ - + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt b/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt index 247b5282bc..7bb321b372 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt @@ -3,13 +3,23 @@ package io.github.sds100.keymapper.base import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.SystemClock +import timber.log.Timber class BootBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { context ?: return - if (intent?.action == Intent.ACTION_LOCKED_BOOT_COMPLETED) { - (context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked() + when (intent?.action) { + Intent.ACTION_BOOT_COMPLETED -> { + Timber.i( + "Boot completed broadcast: time since boot = ${SystemClock.elapsedRealtime() / 1000}", + ) + } + + Intent.ACTION_LOCKED_BOOT_COMPLETED -> { + (context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked() + } } } } 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 55b487ceba..a79591be43 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 @@ -27,6 +27,7 @@ import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.permissions.SystemFeatureAdapter import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import io.github.sds100.keymapper.system.settings.SettingType +import java.util.concurrent.ConcurrentHashMap class LazyActionErrorSnapshot( private val packageManager: PackageManagerAdapter, @@ -65,6 +66,9 @@ class LazyActionErrorSnapshot( } } + private val isAppEnabledCache = ConcurrentHashMap() + private val isAppInstalledCache = ConcurrentHashMap() + private val isSystemBridgeConnected: Boolean by lazy { systemBridgeConnectionManager.isConnected() } @@ -250,13 +254,30 @@ class LazyActionErrorSnapshot( } private fun getAppError(packageName: String): KMError? { + if (isAppEnabledCache.contains(packageName) && isAppInstalledCache.contains(packageName)) { + if (isAppEnabledCache[packageName] == false) { + return KMError.AppDisabled(packageName) + } + + if (isAppInstalledCache[packageName] == false) { + return KMError.AppDisabled(packageName) + } + + return null + } + + val isAppInstalled = packageManager.isAppInstalled(packageName) + isAppInstalledCache[packageName] = isAppInstalled + packageManager.isAppEnabled(packageName).onSuccess { isEnabled -> + isAppEnabledCache[packageName] = isEnabled + if (!isEnabled) { return KMError.AppDisabled(packageName) } } - if (!packageManager.isAppInstalled(packageName)) { + if (!isAppInstalled) { return KMError.AppNotFound(packageName) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt index ded1fa8050..3525167f70 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt @@ -843,7 +843,7 @@ class CreateActionDelegate( "create_text_action", DialogModel.Text( hint = getString(R.string.hint_create_text_action), - allowEmpty = false, + allowEmpty = true, text = oldText, ), ) ?: return null diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt index 1bca36c182..81ca3c2fc1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt @@ -78,9 +78,11 @@ fun FixKeyEventActionBottomSheet( ) { Column( modifier = Modifier + .animateContentSize() + .verticalScroll(rememberScrollState()) .padding(16.dp) .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { Text( modifier = Modifier.align(Alignment.CenterHorizontally), @@ -90,24 +92,68 @@ fun FixKeyEventActionBottomSheet( overflow = TextOverflow.Ellipsis, ) - Column( - modifier = Modifier - .animateContentSize() - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp), + Text(stringResource(R.string.fix_key_event_action_text)) + + FixKeyEventActionOptionCard( + onClick = onSelectInputMethod, + selected = state is FixKeyEventActionState.InputMethod, + title = stringResource(R.string.fix_key_event_action_input_method_title), + icon = Icons.Rounded.Keyboard, ) { - Text(stringResource(R.string.fix_key_event_action_text)) + val annotatedText = buildAnnotatedString { + appendInlineContent("icon", "[icon]") + append(" ") + append(stringResource(R.string.fix_key_event_action_input_method_text)) + } + val inlineContent = mapOf( + Pair( + "icon", + InlineTextContent( + Placeholder( + width = MaterialTheme.typography.bodyLarge.fontSize, + height = MaterialTheme.typography.bodyLarge.fontSize, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + Icon( + imageVector = Icons.Rounded.Remove, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + }, + ), + ) + Text( + annotatedText, + inlineContent = inlineContent, + style = MaterialTheme.typography.bodyMedium, + ) + } - FixKeyEventActionOptionCard( - onClick = onSelectInputMethod, - selected = state is FixKeyEventActionState.InputMethod, - title = stringResource(R.string.fix_key_event_action_input_method_title), - icon = Icons.Rounded.Keyboard, - ) { + val isExpertModeUnsupported = state.expertModeStatus == ExpertModeStatus.UNSUPPORTED + + FixKeyEventActionOptionCard( + onClick = onSelectExpertMode, + selected = state is FixKeyEventActionState.ExpertMode, + title = stringResource(R.string.expert_mode_app_bar_title), + icon = Icons.Outlined.OfflineBolt, + enabled = !isExpertModeUnsupported, + ) { + if (isExpertModeUnsupported) { + Text( + stringResource(R.string.trigger_setup_expert_mode_unsupported), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + ) + } else { val annotatedText = buildAnnotatedString { appendInlineContent("icon", "[icon]") append(" ") - append(stringResource(R.string.fix_key_event_action_input_method_text)) + append(stringResource(R.string.fix_key_event_action_expert_mode_text_1)) + appendLine() + appendInlineContent("icon", "[icon]") + append(" ") + append(stringResource(R.string.fix_key_event_action_expert_mode_text_2)) } val inlineContent = mapOf( Pair( @@ -116,13 +162,14 @@ fun FixKeyEventActionBottomSheet( Placeholder( width = MaterialTheme.typography.bodyLarge.fontSize, height = MaterialTheme.typography.bodyLarge.fontSize, - placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + placeholderVerticalAlign = + PlaceholderVerticalAlign.TextCenter, ), ) { Icon( - imageVector = Icons.Rounded.Remove, + imageVector = Icons.Rounded.Add, contentDescription = null, - tint = MaterialTheme.colorScheme.error, + tint = LocalCustomColorsPalette.current.green, ) }, ), @@ -133,65 +180,13 @@ fun FixKeyEventActionBottomSheet( style = MaterialTheme.typography.bodyMedium, ) } - - val isExpertModeUnsupported = state.expertModeStatus == ExpertModeStatus.UNSUPPORTED - - FixKeyEventActionOptionCard( - onClick = onSelectExpertMode, - selected = state is FixKeyEventActionState.ExpertMode, - title = stringResource(R.string.expert_mode_app_bar_title), - icon = Icons.Outlined.OfflineBolt, - enabled = !isExpertModeUnsupported, - ) { - if (isExpertModeUnsupported) { - Text( - stringResource(R.string.trigger_setup_expert_mode_unsupported), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error, - ) - } else { - val annotatedText = buildAnnotatedString { - appendInlineContent("icon", "[icon]") - append(" ") - append(stringResource(R.string.fix_key_event_action_expert_mode_text_1)) - appendLine() - appendInlineContent("icon", "[icon]") - append(" ") - append(stringResource(R.string.fix_key_event_action_expert_mode_text_2)) - } - val inlineContent = mapOf( - Pair( - "icon", - InlineTextContent( - Placeholder( - width = MaterialTheme.typography.bodyLarge.fontSize, - height = MaterialTheme.typography.bodyLarge.fontSize, - placeholderVerticalAlign = - PlaceholderVerticalAlign.TextCenter, - ), - ) { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = null, - tint = LocalCustomColorsPalette.current.green, - ) - }, - ), - ) - Text( - annotatedText, - inlineContent = inlineContent, - style = MaterialTheme.typography.bodyMedium, - ) - } - } - - Text( - stringResource(R.string.fix_key_event_action_change_in_settings_caption), - style = MaterialTheme.typography.labelMedium, - ) } + Text( + stringResource(R.string.fix_key_event_action_change_in_settings_caption), + style = MaterialTheme.typography.labelMedium, + ) + HeaderText(text = stringResource(R.string.fix_key_event_action_setup_title)) AccessibilityServiceRequirementRow( @@ -234,7 +229,10 @@ fun FixKeyEventActionBottomSheet( } } - Button(modifier = Modifier.align(Alignment.End), onClick = onDoneClick) { + Button( + modifier = Modifier.align(Alignment.End), + onClick = onDoneClick, + ) { Text(stringResource(R.string.pos_done)) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt index a895c2b4c2..36959ac9b6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt @@ -802,6 +802,8 @@ class KeyMapAlgorithm( val detectedShortPressTriggers = mutableSetOf() val vibrateDurations = mutableListOf() + val errorSnapshot = performActionsUseCase.getErrorSnapshot() + /* loop through triggers in a different loop first to increment the last matched index. Otherwise the order of the key maps affects the logic. @@ -815,8 +817,6 @@ class KeyMapAlgorithm( val lastMatchedIndex = lastMatchedEventIndices[triggerIndex] - val errorSnapshot = performActionsUseCase.getErrorSnapshot() - val actionList = triggerActions[triggerIndex] .map { actionKey -> actionMap[actionKey]?.data } .filterNotNull() 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 e98a45c12a..c54298cffa 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 @@ -110,12 +110,17 @@ class SystemBridgeAutoStarter @Inject constructor( useShizukuFlow.flatMapLatest { useShizuku -> if (useShizuku) { + Timber.i("autoStartTypeFlow: Use shizuku") flowOf(AutoStartEligibility.Eligible(AutoStartType.SHIZUKU)) } else if (buildConfig.sdkInt >= Build.VERSION_CODES.R) { + Timber.i("autoStartTypeFlow: Do not use shizuku") combine( permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), networkAdapter.isWifiConnected, ) { isWriteSecureSettingsGranted, isWifiConnected -> + Timber.i( + "autoStartTypeFlow: Write secure settings: $isWriteSecureSettingsGranted, Wifi connected: $isWifiConnected", + ) when { !isWifiConnected -> { AutoStartEligibility.NotEligible.WiFiDisconnected diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt index ca911abea6..929038eb47 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt @@ -323,7 +323,7 @@ class KeyMapListItemCreator( append(key.getCodeLabel(this@KeyMapListItemCreator)) val parts = buildList { - add("PRO") + add("Expert") add(key.device.name) if (!key.consumeEvent) { diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 46249c87f1..7d694ae30b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -71,7 +71,9 @@ lane :production do supply( aab: "../app/build/outputs/bundle/release/app-release.aab", track: "internal", - skip_upload_apk: true + skip_upload_apk: true, + # Skip uploading the title because F-droid should not have "Floating buttons" in its title. + skip_upload_metadata: true, ) whats_new = File.read("../base/src/main/assets/whats-new.txt") diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt index 9810cafe1e..19f819ebd7 100644 --- a/fastlane/metadata/android/en-US/title.txt +++ b/fastlane/metadata/android/en-US/title.txt @@ -1 +1 @@ -Key Mapper & Floating Buttons \ No newline at end of file +Key Mapper \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/title.txt b/fastlane/metadata/android/tr_TR/title.txt index 9810cafe1e..19f819ebd7 100644 --- a/fastlane/metadata/android/tr_TR/title.txt +++ b/fastlane/metadata/android/tr_TR/title.txt @@ -1 +1 @@ -Key Mapper & Floating Buttons \ No newline at end of file +Key Mapper \ No newline at end of file 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 565c890de4..8cb911dda8 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 @@ -285,7 +285,9 @@ class SystemBridgeSetupControllerImpl @Inject constructor( isAdbPairedJob?.cancel() isAdbPairedResult.value = null + Timber.d("Launching isAdbPaired job") isAdbPairedJob = coroutineScope.launch { + Timber.d("Enabling wireless ADB") SettingsUtils.putGlobalSetting(ctx, ADB_WIRELESS_SETTING, 1) // Try running a command to see if the pairing is working correctly.