Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/version.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VERSION_NAME=4.0.4
VERSION_CODE=246
VERSION_NAME=4.0.5
VERSION_CODE=247
13 changes: 12 additions & 1 deletion base/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
Expand Down Expand Up @@ -117,5 +118,15 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>

<receiver
android:name=".BootBroadcastReceiver"
android:directBootAware="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -65,6 +66,9 @@ class LazyActionErrorSnapshot(
}
}

private val isAppEnabledCache = ConcurrentHashMap<String, Boolean>()
private val isAppInstalledCache = ConcurrentHashMap<String, Boolean>()

private val isSystemBridgeConnected: Boolean by lazy {
systemBridgeConnectionManager.isConnected()
}
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(
Expand All @@ -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,
)
},
),
Expand All @@ -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(
Expand Down Expand Up @@ -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))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,8 @@ class KeyMapAlgorithm(
val detectedShortPressTriggers = mutableSetOf<Int>()
val vibrateDurations = mutableListOf<Long>()

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.
Expand All @@ -815,8 +817,6 @@ class KeyMapAlgorithm(

val lastMatchedIndex = lastMatchedEventIndices[triggerIndex]

val errorSnapshot = performActionsUseCase.getErrorSnapshot()

val actionList = triggerActions[triggerIndex]
.map { actionKey -> actionMap[actionKey]?.data }
.filterNotNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion fastlane/metadata/android/en-US/title.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Key Mapper & Floating Buttons
Key Mapper
2 changes: 1 addition & 1 deletion fastlane/metadata/android/tr_TR/title.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Key Mapper & Floating Buttons
Key Mapper
Loading
Loading