From 0c88cd6637fd9430271906fa5df469aaf7a5058f Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 24 Jun 2026 10:28:30 +0200 Subject: [PATCH 01/24] bump version to 4.2.2 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index 0e1183310c..eb15c83b69 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.2.1 -VERSION_CODE=256 +VERSION_NAME=4.2.2 +VERSION_CODE=257 From bc11b09c946c798abb7edafdedc5baea16425343 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 24 Jun 2026 10:28:50 +0200 Subject: [PATCH 02/24] update foss paywall text --- base/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index e6561e8dc0..bc808ec65f 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1530,9 +1530,9 @@ 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. Key Mapper FOSS support Floating Buttons and Assistant trigger aren\'t sold in this FOSS build because RevenueCat and Google Play in-app billing aren\'t included.\n\nYou can still support development on Ko-fi. - If you donate and later want those products,\ + If you donate and later get Google Play on another device,\ contact us - \ and we can usually help convert your support so it counts toward Google Play purchases. + \ and we will give you the paid features so you do not need to pay again. Your support helps keep Key Mapper largely open source, without ads or tracking. We don\'t sell your personal data. Donations help fund Android compatibility updates, bug fixes and maintenance, improvements and documentation, and keeping the project sustainable. Support on Ko-fi From 389a17dd62850a9c88d81727cf7e2d6df3afe6b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 00:27:12 +0000 Subject: [PATCH 03/24] #2163 feat: add ringer mode constraints Add three new constraints to check the current ringer mode: Ring, Vibrate, and Silent. These appear in the Phone category of the constraint picker. - Add RINGER_MODE_NORMAL, RINGER_MODE_VIBRATE, RINGER_MODE_SILENT to ConstraintId - Add corresponding ConstraintData sealed objects and entity mapping - Add ringerModeFlow to VolumeAdapter, implemented via BroadcastReceiver for AudioManager.RINGER_MODE_CHANGED_ACTION in AndroidVolumeAdapter - Wire VolumeAdapter into LazyConstraintSnapshot and DetectConstraintsUseCase - Add RINGER_MODE to ConstraintDependency - Add UI strings, icons, and titles - Update TestConstraintSnapshot with ringerMode field --- CHANGELOG.md | 1 + .../keymapper/base/constraints/Constraint.kt | 34 +++++++++++++++++++ .../base/constraints/ConstraintDependency.kt | 1 + .../base/constraints/ConstraintId.kt | 4 +++ .../base/constraints/ConstraintSnapshot.kt | 8 +++++ .../base/constraints/ConstraintUiHelper.kt | 3 ++ .../base/constraints/ConstraintUtils.kt | 12 +++++++ .../constraints/DetectConstraintsUseCase.kt | 4 +++ base/src/main/res/values/strings.xml | 3 ++ .../base/utils/TestConstraintSnapshot.kt | 5 +++ .../data/entities/ConstraintEntity.kt | 4 +++ .../system/volume/AndroidVolumeAdapter.kt | 31 +++++++++++++++++ .../keymapper/system/volume/VolumeAdapter.kt | 2 ++ 13 files changed, 112 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ca051484..b3cc9aa9b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Added +- #2163 Add ringer mode constraints (Ring, Vibrate, Silent). - #2140 Add monochrome app icon layer for themed icon support on Android 13+. ## Fixed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt index a2aa471fbd..e8cf0a8361 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt @@ -189,6 +189,21 @@ sealed class ConstraintData { override val id: ConstraintId = ConstraintId.PHONE_RINGING } + @Serializable + data object RingerModeNormal : ConstraintData() { + override val id: ConstraintId = ConstraintId.RINGER_MODE_NORMAL + } + + @Serializable + data object RingerModeVibrate : ConstraintData() { + override val id: ConstraintId = ConstraintId.RINGER_MODE_VIBRATE + } + + @Serializable + data object RingerModeSilent : ConstraintData() { + override val id: ConstraintId = ConstraintId.RINGER_MODE_SILENT + } + @Serializable data object Charging : ConstraintData() { override val id: ConstraintId = ConstraintId.CHARGING @@ -382,6 +397,10 @@ object ConstraintEntityMapper { ConstraintEntity.IN_PHONE_CALL -> ConstraintData.InPhoneCall ConstraintEntity.NOT_IN_PHONE_CALL -> ConstraintData.NotInPhoneCall + ConstraintEntity.RINGER_MODE_NORMAL -> ConstraintData.RingerModeNormal + ConstraintEntity.RINGER_MODE_VIBRATE -> ConstraintData.RingerModeVibrate + ConstraintEntity.RINGER_MODE_SILENT -> ConstraintData.RingerModeSilent + ConstraintEntity.CHARGING -> ConstraintData.Charging ConstraintEntity.DISCHARGING -> ConstraintData.Discharging @@ -673,6 +692,21 @@ object ConstraintEntityMapper { ConstraintEntity.PHONE_RINGING, ) + is ConstraintData.RingerModeNormal -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.RINGER_MODE_NORMAL, + ) + + is ConstraintData.RingerModeVibrate -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.RINGER_MODE_VIBRATE, + ) + + is ConstraintData.RingerModeSilent -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.RINGER_MODE_SILENT, + ) + is ConstraintData.Charging -> ConstraintEntity( uid = constraint.uid, ConstraintEntity.CHARGING, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt index 4bbbcf1721..afab1a49ca 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt @@ -16,6 +16,7 @@ enum class ConstraintDependency { DEVICE_LOCKED_STATE, LOCK_SCREEN_SHOWING, PHONE_STATE, + RINGER_MODE, CHARGING_STATE, HINGE_STATE, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt index 06463c6fd7..1118a9d186 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt @@ -54,6 +54,10 @@ enum class ConstraintId { NOT_IN_PHONE_CALL, PHONE_RINGING, + RINGER_MODE_NORMAL, + RINGER_MODE_VIBRATE, + RINGER_MODE_SILENT, + CHARGING, DISCHARGING, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt index 00ac3781ee..9ee0521dcb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt @@ -21,6 +21,8 @@ import io.github.sds100.keymapper.system.network.NetworkAdapter import io.github.sds100.keymapper.system.phone.CallState import io.github.sds100.keymapper.system.phone.PhoneAdapter import io.github.sds100.keymapper.system.power.PowerAdapter +import io.github.sds100.keymapper.system.volume.RingerMode +import io.github.sds100.keymapper.system.volume.VolumeAdapter import java.time.LocalTime /** @@ -38,6 +40,7 @@ class LazyConstraintSnapshot( phoneAdapter: PhoneAdapter, powerAdapter: PowerAdapter, private val foldableAdapter: FoldableAdapter, + volumeAdapter: VolumeAdapter, ) : ConstraintSnapshot { private val appInForeground: String? by lazy { accessibilityService.rootNode?.packageName } private val connectedBluetoothDevices: Set by lazy { @@ -66,6 +69,7 @@ class LazyConstraintSnapshot( } private val callState: CallState by lazy { phoneAdapter.getCallState() } private val isCharging: Boolean by lazy { powerAdapter.isCharging.value } + private val ringerMode: RingerMode by lazy { volumeAdapter.ringerMode } private val isLocked: Boolean by lazy { lockScreenAdapter.isLocked() @@ -165,6 +169,10 @@ class LazyConstraintSnapshot( callState == CallState.RINGING || audioVolumeStreams.contains(AudioManager.STREAM_RING) + is ConstraintData.RingerModeNormal -> ringerMode == RingerMode.NORMAL + is ConstraintData.RingerModeVibrate -> ringerMode == RingerMode.VIBRATE + is ConstraintData.RingerModeSilent -> ringerMode == RingerMode.SILENT + is ConstraintData.Charging -> isCharging is ConstraintData.Discharging -> !isCharging diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt index 94b02aebc9..e9bb6064f4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt @@ -163,6 +163,9 @@ class ConstraintUiHelper( is ConstraintData.InPhoneCall -> getString(R.string.constraint_in_phone_call) is ConstraintData.NotInPhoneCall -> getString(R.string.constraint_not_in_phone_call) is ConstraintData.PhoneRinging -> getString(R.string.constraint_phone_ringing) + is ConstraintData.RingerModeNormal -> getString(R.string.constraint_ringer_mode_normal) + is ConstraintData.RingerModeVibrate -> getString(R.string.constraint_ringer_mode_vibrate) + is ConstraintData.RingerModeSilent -> getString(R.string.constraint_ringer_mode_silent) is ConstraintData.Charging -> getString(R.string.constraint_charging) is ConstraintData.Discharging -> getString(R.string.constraint_discharging) is ConstraintData.HingeClosed -> getString(R.string.constraint_hinge_closed_description) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index 80cd56c1c7..7bb26b672f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -15,6 +15,9 @@ import androidx.compose.material.icons.outlined.KeyboardHide import androidx.compose.material.icons.outlined.Lock import androidx.compose.material.icons.outlined.LockOpen import androidx.compose.material.icons.outlined.MobileOff +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.NotificationsOff +import androidx.compose.material.icons.outlined.Vibration import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.RingVolume import androidx.compose.material.icons.outlined.ScreenLockPortrait @@ -101,6 +104,9 @@ object ConstraintUtils { ConstraintId.IN_PHONE_CALL, ConstraintId.NOT_IN_PHONE_CALL, ConstraintId.PHONE_RINGING, + ConstraintId.RINGER_MODE_NORMAL, + ConstraintId.RINGER_MODE_VIBRATE, + ConstraintId.RINGER_MODE_SILENT, -> ConstraintCategory.PHONE ConstraintId.CHARGING, @@ -180,6 +186,9 @@ object ConstraintUtils { ConstraintId.IN_PHONE_CALL -> ComposeIconInfo.Vector(Icons.Outlined.Call) ConstraintId.NOT_IN_PHONE_CALL -> ComposeIconInfo.Vector(Icons.Outlined.CallEnd) ConstraintId.PHONE_RINGING -> ComposeIconInfo.Vector(Icons.Outlined.RingVolume) + ConstraintId.RINGER_MODE_NORMAL -> ComposeIconInfo.Vector(Icons.Outlined.Notifications) + ConstraintId.RINGER_MODE_VIBRATE -> ComposeIconInfo.Vector(Icons.Outlined.Vibration) + ConstraintId.RINGER_MODE_SILENT -> ComposeIconInfo.Vector(Icons.Outlined.NotificationsOff) ConstraintId.CHARGING -> ComposeIconInfo.Vector(Icons.Outlined.BatteryChargingFull) ConstraintId.DISCHARGING -> ComposeIconInfo.Vector(Icons.Outlined.Battery2Bar) @@ -236,6 +245,9 @@ object ConstraintUtils { ConstraintId.IN_PHONE_CALL -> R.string.constraint_in_phone_call ConstraintId.NOT_IN_PHONE_CALL -> R.string.constraint_not_in_phone_call ConstraintId.PHONE_RINGING -> R.string.constraint_phone_ringing + ConstraintId.RINGER_MODE_NORMAL -> R.string.constraint_ringer_mode_normal + ConstraintId.RINGER_MODE_VIBRATE -> R.string.constraint_ringer_mode_vibrate + ConstraintId.RINGER_MODE_SILENT -> R.string.constraint_ringer_mode_silent ConstraintId.CHARGING -> R.string.constraint_charging ConstraintId.DISCHARGING -> R.string.constraint_discharging ConstraintId.HINGE_CLOSED -> R.string.constraint_hinge_closed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt index 8bf75d6f24..23da542247 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt @@ -16,6 +16,7 @@ import io.github.sds100.keymapper.system.media.MediaAdapter import io.github.sds100.keymapper.system.network.NetworkAdapter import io.github.sds100.keymapper.system.phone.PhoneAdapter import io.github.sds100.keymapper.system.power.PowerAdapter +import io.github.sds100.keymapper.system.volume.VolumeAdapter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map @@ -34,6 +35,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( private val phoneAdapter: PhoneAdapter, private val powerAdapter: PowerAdapter, private val foldableAdapter: FoldableAdapter, + private val volumeAdapter: VolumeAdapter, ) : DetectConstraintsUseCase { @AssistedFactory @@ -53,6 +55,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( phoneAdapter, powerAdapter, foldableAdapter, + volumeAdapter, ) override fun onDependencyChanged(dependency: ConstraintDependency): Flow { @@ -92,6 +95,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ).map { dependency } ConstraintDependency.PHONE_STATE -> phoneAdapter.callStateFlow.map { dependency } + ConstraintDependency.RINGER_MODE -> volumeAdapter.ringerModeFlow.map { dependency } ConstraintDependency.CHARGING_STATE -> powerAdapter.isCharging.map { dependency } ConstraintDependency.HINGE_STATE -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index e6561e8dc0..a10a98b4e1 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -297,6 +297,9 @@ In phone call Not in phone call Phone ringing + Ringer mode: Ring + Ringer mode: Vibrate + Ringer mode: Silent Charging Discharging diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt index be77315302..9039b9823a 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt @@ -11,6 +11,7 @@ import io.github.sds100.keymapper.system.foldable.HingeState import io.github.sds100.keymapper.system.foldable.isClosed import io.github.sds100.keymapper.system.foldable.isOpen import io.github.sds100.keymapper.system.phone.CallState +import io.github.sds100.keymapper.system.volume.RingerMode import java.time.LocalTime import timber.log.Timber @@ -26,6 +27,7 @@ class TestConstraintSnapshot( val chosenImeId: String? = null, val isKeyboardShowing: Boolean = false, val callState: CallState = CallState.NONE, + val ringerMode: RingerMode = RingerMode.NORMAL, val isCharging: Boolean = false, val isLocked: Boolean = false, val isBackFlashlightOn: Boolean = false, @@ -107,6 +109,9 @@ class TestConstraintSnapshot( is ConstraintData.InPhoneCall -> callState == CallState.IN_PHONE_CALL is ConstraintData.NotInPhoneCall -> callState == CallState.NONE is ConstraintData.PhoneRinging -> callState == CallState.RINGING + is ConstraintData.RingerModeNormal -> ringerMode == RingerMode.NORMAL + is ConstraintData.RingerModeVibrate -> ringerMode == RingerMode.VIBRATE + is ConstraintData.RingerModeSilent -> ringerMode == RingerMode.SILENT is ConstraintData.Charging -> isCharging is ConstraintData.Discharging -> !isCharging is ConstraintData.LockScreenShowing -> isLockscreenShowing diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt index 3deebac611..d81bdddddf 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt @@ -88,6 +88,10 @@ data class ConstraintEntity( const val NOT_IN_PHONE_CALL = "not_in_phone_call" const val PHONE_RINGING = "phone_ringing" + const val RINGER_MODE_NORMAL = "ringer_mode_normal" + const val RINGER_MODE_VIBRATE = "ringer_mode_vibrate" + const val RINGER_MODE_SILENT = "ringer_mode_silent" + const val CHARGING = "charging" const val DISCHARGING = "discharging" 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 8a7e3ea0eb..3e6017648e 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 @@ -1,10 +1,14 @@ package io.github.sds100.keymapper.system.volume import android.app.NotificationManager +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.media.AudioManager import android.os.Build import android.provider.Settings +import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMResult @@ -18,6 +22,9 @@ import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import timber.log.Timber @Singleton @@ -30,6 +37,30 @@ class AndroidVolumeAdapter @Inject constructor( private val audioManager: AudioManager by lazy { ctx.getSystemService()!! } private val notificationManager: NotificationManager by lazy { ctx.getSystemService()!! } + private val ringerModeReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent ?: return + if (intent.action == AudioManager.RINGER_MODE_CHANGED_ACTION) { + _ringerModeFlow.value = ringerMode + } + } + } + + private val _ringerModeFlow: MutableStateFlow by lazy { + MutableStateFlow(ringerMode) + } + override val ringerModeFlow: StateFlow by lazy { _ringerModeFlow.asStateFlow() } + + init { + val filter = IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION) + ContextCompat.registerReceiver( + ctx, + ringerModeReceiver, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + } + override val ringerMode: RingerMode get() { // Get the current ringer mode with the setting because the AudioManager diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt index 7bae48cc79..c5c88f8961 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt @@ -1,9 +1,11 @@ package io.github.sds100.keymapper.system.volume import io.github.sds100.keymapper.common.utils.KMResult +import kotlinx.coroutines.flow.Flow interface VolumeAdapter { val ringerMode: RingerMode + val ringerModeFlow: Flow fun raiseVolume(stream: VolumeStream? = null, showVolumeUi: Boolean): KMResult<*> fun lowerVolume(stream: VolumeStream? = null, showVolumeUi: Boolean): KMResult<*> From 106416a4fe91055cf1c3b2f7a800cabf83c40681 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 00:37:09 +0000 Subject: [PATCH 04/24] #2174 feat: add default do not remap preference Add a "Do not remap by default" toggle to the Change Default Options settings page. When enabled, new trigger keys will have consumeEvent = false by default, letting the original key event pass through to other apps. --- CHANGELOG.md | 1 + .../keymaps/GetDefaultKeyMapOptionsUseCase.kt | 6 ++ .../settings/DefaultOptionsSettingsScreen.kt | 13 +++++ .../base/settings/SettingsViewModel.kt | 55 ++++++++++++------- .../base/trigger/ConfigTriggerDelegate.kt | 6 +- .../base/trigger/ConfigTriggerUseCase.kt | 2 + base/src/main/res/values/strings.xml | 3 + .../io/github/sds100/keymapper/data/Keys.kt | 1 + .../keymapper/data/PreferenceDefaults.kt | 2 + 9 files changed, 66 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2cb693525..366451a8ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ## Added - #2140 Add monochrome app icon layer for themed icon support on Android 13+. +- #2174 Add "Do not remap by default" preference to the default options settings page. ## Fixed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt index 89fa437515..24d8fb2034 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt @@ -55,6 +55,11 @@ class GetDefaultKeyMapOptionsUseCaseImpl @Inject constructor( preferenceRepository.get(Keys.defaultVibrateDuration) .map { it ?: PreferenceDefaults.VIBRATION_DURATION } .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.VIBRATION_DURATION) + + override val defaultDoNotRemap: StateFlow = + preferenceRepository.get(Keys.defaultDoNotRemap) + .map { it ?: PreferenceDefaults.DO_NOT_REMAP } + .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.DO_NOT_REMAP) } interface GetDefaultKeyMapOptionsUseCase { @@ -65,4 +70,5 @@ interface GetDefaultKeyMapOptionsUseCase { val defaultDoublePressDelay: StateFlow val defaultSequenceTriggerTimeout: StateFlow val defaultVibrateDuration: StateFlow + val defaultDoNotRemap: StateFlow } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt index 2d807b2042..b36d4e4fc9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Keyboard import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -35,6 +36,7 @@ import io.github.sds100.keymapper.base.utils.ui.SliderMaximums import io.github.sds100.keymapper.base.utils.ui.SliderMinimums import io.github.sds100.keymapper.base.utils.ui.SliderStepSizes import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText +import io.github.sds100.keymapper.base.utils.ui.compose.SwitchPreferenceCompose @Composable fun DefaultOptionsSettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { @@ -209,6 +211,16 @@ private fun Content( stepSize = SliderStepSizes.TRIGGER_SEQUENCE_TRIGGER_TIMEOUT, ) Spacer(Modifier.height(8.dp)) + + // Do not remap + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_default_do_not_remap), + text = stringResource(R.string.summary_pref_default_do_not_remap), + icon = Icons.Rounded.Keyboard, + isChecked = state.doNotRemap, + onCheckedChange = { callback.onDefaultDoNotRemapChanged(it) }, + ) + Spacer(Modifier.height(8.dp)) } } @@ -219,6 +231,7 @@ interface DefaultOptionsSettingsCallback { fun onRepeatDelayChanged(delay: Int) = run { } fun onRepeatRateChanged(rate: Int) = run { } fun onSequenceTriggerTimeoutChanged(timeout: Int) = run { } + fun onDefaultDoNotRemapChanged(doNotRemap: Boolean) = run { } } @Preview diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 1a2ef5dbda..57dd804b14 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -72,27 +72,32 @@ class SettingsViewModel @Inject constructor( }.stateIn(viewModelScope, SharingStarted.Lazily, MainSettingsState()) val defaultSettingsScreenState: StateFlow = combine( - useCase.getPreference(Keys.defaultLongPressDelay), - useCase.getPreference(Keys.defaultDoublePressDelay), - useCase.getPreference(Keys.defaultVibrateDuration), - useCase.getPreference(Keys.defaultRepeatDelay), - useCase.getPreference(Keys.defaultRepeatRate), - useCase.getPreference(Keys.defaultSequenceTriggerTimeout), - ) { values -> - DefaultSettingsState( - longPressDelay = values[0] ?: PreferenceDefaults.LONG_PRESS_DELAY, - defaultLongPressDelay = PreferenceDefaults.LONG_PRESS_DELAY, - doublePressDelay = values[1] ?: PreferenceDefaults.DOUBLE_PRESS_DELAY, - defaultDoublePressDelay = PreferenceDefaults.DOUBLE_PRESS_DELAY, - vibrateDuration = values[2] ?: PreferenceDefaults.VIBRATION_DURATION, - defaultVibrateDuration = PreferenceDefaults.VIBRATION_DURATION, - repeatDelay = values[3] ?: PreferenceDefaults.REPEAT_DELAY, - defaultRepeatDelay = PreferenceDefaults.REPEAT_DELAY, - repeatRate = values[4] ?: PreferenceDefaults.REPEAT_RATE, - defaultRepeatRate = PreferenceDefaults.REPEAT_RATE, - sequenceTriggerTimeout = values[5] ?: PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, - defaultSequenceTriggerTimeout = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, - ) + combine( + useCase.getPreference(Keys.defaultLongPressDelay), + useCase.getPreference(Keys.defaultDoublePressDelay), + useCase.getPreference(Keys.defaultVibrateDuration), + useCase.getPreference(Keys.defaultRepeatDelay), + useCase.getPreference(Keys.defaultRepeatRate), + useCase.getPreference(Keys.defaultSequenceTriggerTimeout), + ) { values -> + DefaultSettingsState( + longPressDelay = values[0] ?: PreferenceDefaults.LONG_PRESS_DELAY, + defaultLongPressDelay = PreferenceDefaults.LONG_PRESS_DELAY, + doublePressDelay = values[1] ?: PreferenceDefaults.DOUBLE_PRESS_DELAY, + defaultDoublePressDelay = PreferenceDefaults.DOUBLE_PRESS_DELAY, + vibrateDuration = values[2] ?: PreferenceDefaults.VIBRATION_DURATION, + defaultVibrateDuration = PreferenceDefaults.VIBRATION_DURATION, + repeatDelay = values[3] ?: PreferenceDefaults.REPEAT_DELAY, + defaultRepeatDelay = PreferenceDefaults.REPEAT_DELAY, + repeatRate = values[4] ?: PreferenceDefaults.REPEAT_RATE, + defaultRepeatRate = PreferenceDefaults.REPEAT_RATE, + sequenceTriggerTimeout = values[5] ?: PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, + defaultSequenceTriggerTimeout = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, + ) + }, + useCase.getPreference(Keys.defaultDoNotRemap), + ) { state, doNotRemap -> + state.copy(doNotRemap = doNotRemap ?: PreferenceDefaults.DO_NOT_REMAP) }.stateIn(viewModelScope, SharingStarted.Lazily, DefaultSettingsState()) val automaticChangeImeSettingsState: StateFlow = combine( @@ -241,6 +246,12 @@ class SettingsViewModel @Inject constructor( } } + override fun onDefaultDoNotRemapChanged(doNotRemap: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultDoNotRemap, doNotRemap) + } + } + fun onShowToastWhenAutoChangingImeToggled(enabled: Boolean) { viewModelScope.launch { useCase.setPreference(Keys.showToastWhenAutoChangingIme, enabled) @@ -387,6 +398,8 @@ data class DefaultSettingsState( val sequenceTriggerTimeout: Int = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, val defaultSequenceTriggerTimeout: Int = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, + + val doNotRemap: Boolean = PreferenceDefaults.DO_NOT_REMAP, ) data class AutomaticChangeImeSettingsState( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt index 65c30dfc77..3a6d6265fb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -66,6 +66,7 @@ class ConfigTriggerDelegate { device: KeyEventTriggerDevice, requiresIme: Boolean, otherTriggerKeys: List = emptyList(), + defaultDoNotRemap: Boolean = false, ): Trigger { val isPowerKey = KeyEventUtils.isPowerButtonKey(keyCode, scanCode) @@ -79,7 +80,7 @@ class ConfigTriggerDelegate { } } - var consumeKeyEvent = true + var consumeKeyEvent = !defaultDoNotRemap // Issue #753 if (KeyEventUtils.isModifierKey(keyCode)) { @@ -130,6 +131,7 @@ class ConfigTriggerDelegate { scanCode: Int, device: EvdevDeviceInfo, otherTriggerKeys: List = emptyList(), + defaultDoNotRemap: Boolean = false, ): Trigger { val isPowerKey = KeyEventUtils.isPowerButtonKey(keyCode, scanCode) @@ -158,7 +160,7 @@ class ConfigTriggerDelegate { scanCode = scanCode, device = device, clickType = clickType, - consumeEvent = true, + consumeEvent = !defaultDoNotRemap, detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty(), ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt index 9c6a10005e..e24483c034 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -118,6 +118,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( device, requiresIme, otherTriggerKeys = otherTriggerKeys, + defaultDoNotRemap = defaultDoNotRemap.value, ) } @@ -129,6 +130,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( scanCode, device, otherTriggerKeys = otherTriggerKeys, + defaultDoNotRemap = defaultDoNotRemap.value, ) } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index bc808ec65f..c4d5de8e33 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -674,6 +674,9 @@ Change default options For triggers and actions + Do not remap by default + New trigger keys will not consume the key event by default + Reset all DANGER! Are you sure you want to reset all settings in the app to the default? Your existing key maps will not be affected. The introductions and warning pop ups will show again. diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 59e88eb0a4..2e0ec15787 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -40,6 +40,7 @@ object Keys { val defaultRepeatRate = intPreferencesKey("pref_repeat_delay") val defaultSequenceTriggerTimeout = intPreferencesKey("pref_sequence_trigger_timeout") val defaultHoldDownDuration = intPreferencesKey("pref_hold_down_duration") + val defaultDoNotRemap = booleanPreferencesKey("pref_default_do_not_remap") val toggleKeyboardOnToggleKeymaps = booleanPreferencesKey("key_toggle_keyboard_on_pause_resume_keymaps") val automaticBackupLocation = stringPreferencesKey("pref_automatic_backup_location") diff --git a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt index f241647dd6..fe20a9229d 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt @@ -22,4 +22,6 @@ object PreferenceDefaults { // It is false by default and the first time they turn on the system bridge, // the preference will be set to true. const val KEY_EVENT_ACTIONS_USE_SYSTEM_BRIDGE = false + + const val DO_NOT_REMAP = false } From f3398a61dc39f3749dff5ba1fc92ef9219cc46e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 12:20:57 +0000 Subject: [PATCH 05/24] #2163 refactor: replace three RingerMode data objects with single data class Use ConstraintData.RingerMode(val ringerMode: RingerMode) instead of three separate data objects (RingerModeNormal/Vibrate/Silent), following the OrientationCustom pattern. Entity backward compatibility is preserved via the existing three entity type strings. --- .../keymapper/base/constraints/Constraint.kt | 54 +++++++++---------- .../base/constraints/ConstraintSnapshot.kt | 4 +- .../base/constraints/ConstraintUiHelper.kt | 9 ++-- .../base/utils/TestConstraintSnapshot.kt | 4 +- 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt index e8cf0a8361..23181d469d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt @@ -190,18 +190,14 @@ sealed class ConstraintData { } @Serializable - data object RingerModeNormal : ConstraintData() { - override val id: ConstraintId = ConstraintId.RINGER_MODE_NORMAL - } - - @Serializable - data object RingerModeVibrate : ConstraintData() { - override val id: ConstraintId = ConstraintId.RINGER_MODE_VIBRATE - } - - @Serializable - data object RingerModeSilent : ConstraintData() { - override val id: ConstraintId = ConstraintId.RINGER_MODE_SILENT + data class RingerMode( + val ringerMode: io.github.sds100.keymapper.system.volume.RingerMode, + ) : ConstraintData() { + override val id: ConstraintId = when (ringerMode) { + io.github.sds100.keymapper.system.volume.RingerMode.NORMAL -> ConstraintId.RINGER_MODE_NORMAL + io.github.sds100.keymapper.system.volume.RingerMode.VIBRATE -> ConstraintId.RINGER_MODE_VIBRATE + io.github.sds100.keymapper.system.volume.RingerMode.SILENT -> ConstraintId.RINGER_MODE_SILENT + } } @Serializable @@ -397,9 +393,9 @@ object ConstraintEntityMapper { ConstraintEntity.IN_PHONE_CALL -> ConstraintData.InPhoneCall ConstraintEntity.NOT_IN_PHONE_CALL -> ConstraintData.NotInPhoneCall - ConstraintEntity.RINGER_MODE_NORMAL -> ConstraintData.RingerModeNormal - ConstraintEntity.RINGER_MODE_VIBRATE -> ConstraintData.RingerModeVibrate - ConstraintEntity.RINGER_MODE_SILENT -> ConstraintData.RingerModeSilent + ConstraintEntity.RINGER_MODE_NORMAL -> ConstraintData.RingerMode(io.github.sds100.keymapper.system.volume.RingerMode.NORMAL) + ConstraintEntity.RINGER_MODE_VIBRATE -> ConstraintData.RingerMode(io.github.sds100.keymapper.system.volume.RingerMode.VIBRATE) + ConstraintEntity.RINGER_MODE_SILENT -> ConstraintData.RingerMode(io.github.sds100.keymapper.system.volume.RingerMode.SILENT) ConstraintEntity.CHARGING -> ConstraintData.Charging ConstraintEntity.DISCHARGING -> ConstraintData.Discharging @@ -692,20 +688,20 @@ object ConstraintEntityMapper { ConstraintEntity.PHONE_RINGING, ) - is ConstraintData.RingerModeNormal -> ConstraintEntity( - uid = constraint.uid, - ConstraintEntity.RINGER_MODE_NORMAL, - ) - - is ConstraintData.RingerModeVibrate -> ConstraintEntity( - uid = constraint.uid, - ConstraintEntity.RINGER_MODE_VIBRATE, - ) - - is ConstraintData.RingerModeSilent -> ConstraintEntity( - uid = constraint.uid, - ConstraintEntity.RINGER_MODE_SILENT, - ) + is ConstraintData.RingerMode -> when (constraint.data.ringerMode) { + io.github.sds100.keymapper.system.volume.RingerMode.NORMAL -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.RINGER_MODE_NORMAL, + ) + io.github.sds100.keymapper.system.volume.RingerMode.VIBRATE -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.RINGER_MODE_VIBRATE, + ) + io.github.sds100.keymapper.system.volume.RingerMode.SILENT -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.RINGER_MODE_SILENT, + ) + } is ConstraintData.Charging -> ConstraintEntity( uid = constraint.uid, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt index 9ee0521dcb..822bc32361 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt @@ -169,9 +169,7 @@ class LazyConstraintSnapshot( callState == CallState.RINGING || audioVolumeStreams.contains(AudioManager.STREAM_RING) - is ConstraintData.RingerModeNormal -> ringerMode == RingerMode.NORMAL - is ConstraintData.RingerModeVibrate -> ringerMode == RingerMode.VIBRATE - is ConstraintData.RingerModeSilent -> ringerMode == RingerMode.SILENT + is ConstraintData.RingerMode -> ringerMode == constraint.ringerMode is ConstraintData.Charging -> isCharging is ConstraintData.Discharging -> !isCharging diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt index e9bb6064f4..039e922691 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt @@ -11,6 +11,7 @@ import io.github.sds100.keymapper.common.utils.TimeUtils import io.github.sds100.keymapper.common.utils.handle import io.github.sds100.keymapper.common.utils.valueIfFailure import io.github.sds100.keymapper.system.camera.CameraLens +import io.github.sds100.keymapper.system.volume.RingerMode import java.time.format.FormatStyle class ConstraintUiHelper( @@ -163,9 +164,11 @@ class ConstraintUiHelper( is ConstraintData.InPhoneCall -> getString(R.string.constraint_in_phone_call) is ConstraintData.NotInPhoneCall -> getString(R.string.constraint_not_in_phone_call) is ConstraintData.PhoneRinging -> getString(R.string.constraint_phone_ringing) - is ConstraintData.RingerModeNormal -> getString(R.string.constraint_ringer_mode_normal) - is ConstraintData.RingerModeVibrate -> getString(R.string.constraint_ringer_mode_vibrate) - is ConstraintData.RingerModeSilent -> getString(R.string.constraint_ringer_mode_silent) + is ConstraintData.RingerMode -> when (constraint.ringerMode) { + RingerMode.NORMAL -> getString(R.string.constraint_ringer_mode_normal) + RingerMode.VIBRATE -> getString(R.string.constraint_ringer_mode_vibrate) + RingerMode.SILENT -> getString(R.string.constraint_ringer_mode_silent) + } is ConstraintData.Charging -> getString(R.string.constraint_charging) is ConstraintData.Discharging -> getString(R.string.constraint_discharging) is ConstraintData.HingeClosed -> getString(R.string.constraint_hinge_closed_description) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt index 9039b9823a..91598bb35e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt @@ -109,9 +109,7 @@ class TestConstraintSnapshot( is ConstraintData.InPhoneCall -> callState == CallState.IN_PHONE_CALL is ConstraintData.NotInPhoneCall -> callState == CallState.NONE is ConstraintData.PhoneRinging -> callState == CallState.RINGING - is ConstraintData.RingerModeNormal -> ringerMode == RingerMode.NORMAL - is ConstraintData.RingerModeVibrate -> ringerMode == RingerMode.VIBRATE - is ConstraintData.RingerModeSilent -> ringerMode == RingerMode.SILENT + is ConstraintData.RingerMode -> ringerMode == constraint.ringerMode is ConstraintData.Charging -> isCharging is ConstraintData.Discharging -> !isCharging is ConstraintData.LockScreenShowing -> isLockscreenShowing From 30e2f1a6aa247de22c572a1a328c83d1c482cbdf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 12:37:42 +0000 Subject: [PATCH 06/24] #2163 refactor: use single RingerMode constraint data class with ringerMode field Replace three separate data objects (RingerModeNormal, RingerModeVibrate, RingerModeSilent) with one data class ConstraintData.RingerMode(ringerMode: SystemRingerMode), following the OrientationCustom pattern. DB entity backward compatibility is preserved via three separate entity type strings. Add @Serializable to RingerMode enum for kotlinx.serialization support. Fix exhaustive when expressions in ChooseConstraintViewModel and KeyMapConstraintsComparator. --- .../constraints/ChooseConstraintViewModel.kt | 10 +++++++ .../keymapper/base/constraints/Constraint.kt | 26 ++++++++++--------- .../base/constraints/ConstraintUtils.kt | 2 +- .../KeyMapConstraintsComparator.kt | 1 + .../keymapper/system/volume/RingerMode.kt | 3 +++ 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index 836834e723..4f41af9308 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -25,6 +25,7 @@ import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PhysicalOrientation import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.system.camera.CameraLens +import io.github.sds100.keymapper.system.volume.RingerMode import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -249,6 +250,15 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.PHONE_RINGING -> returnResult.emit(ConstraintData.PhoneRinging) + ConstraintId.RINGER_MODE_NORMAL -> + returnResult.emit(ConstraintData.RingerMode(RingerMode.NORMAL)) + + ConstraintId.RINGER_MODE_VIBRATE -> + returnResult.emit(ConstraintData.RingerMode(RingerMode.VIBRATE)) + + ConstraintId.RINGER_MODE_SILENT -> + returnResult.emit(ConstraintData.RingerMode(RingerMode.SILENT)) + ConstraintId.CHARGING -> returnResult.emit(ConstraintData.Charging) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt index 23181d469d..b855f57524 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt @@ -8,6 +8,7 @@ import io.github.sds100.keymapper.data.entities.ConstraintEntity import io.github.sds100.keymapper.data.entities.EntityExtra import io.github.sds100.keymapper.data.entities.getData import io.github.sds100.keymapper.system.camera.CameraLens +import io.github.sds100.keymapper.system.volume.RingerMode as SystemRingerMode import java.time.LocalTime import java.util.UUID import kotlinx.serialization.Serializable @@ -190,13 +191,11 @@ sealed class ConstraintData { } @Serializable - data class RingerMode( - val ringerMode: io.github.sds100.keymapper.system.volume.RingerMode, - ) : ConstraintData() { + data class RingerMode(val ringerMode: SystemRingerMode) : ConstraintData() { override val id: ConstraintId = when (ringerMode) { - io.github.sds100.keymapper.system.volume.RingerMode.NORMAL -> ConstraintId.RINGER_MODE_NORMAL - io.github.sds100.keymapper.system.volume.RingerMode.VIBRATE -> ConstraintId.RINGER_MODE_VIBRATE - io.github.sds100.keymapper.system.volume.RingerMode.SILENT -> ConstraintId.RINGER_MODE_SILENT + SystemRingerMode.NORMAL -> ConstraintId.RINGER_MODE_NORMAL + SystemRingerMode.VIBRATE -> ConstraintId.RINGER_MODE_VIBRATE + SystemRingerMode.SILENT -> ConstraintId.RINGER_MODE_SILENT } } @@ -393,9 +392,12 @@ object ConstraintEntityMapper { ConstraintEntity.IN_PHONE_CALL -> ConstraintData.InPhoneCall ConstraintEntity.NOT_IN_PHONE_CALL -> ConstraintData.NotInPhoneCall - ConstraintEntity.RINGER_MODE_NORMAL -> ConstraintData.RingerMode(io.github.sds100.keymapper.system.volume.RingerMode.NORMAL) - ConstraintEntity.RINGER_MODE_VIBRATE -> ConstraintData.RingerMode(io.github.sds100.keymapper.system.volume.RingerMode.VIBRATE) - ConstraintEntity.RINGER_MODE_SILENT -> ConstraintData.RingerMode(io.github.sds100.keymapper.system.volume.RingerMode.SILENT) + ConstraintEntity.RINGER_MODE_NORMAL -> + ConstraintData.RingerMode(SystemRingerMode.NORMAL) + ConstraintEntity.RINGER_MODE_VIBRATE -> + ConstraintData.RingerMode(SystemRingerMode.VIBRATE) + ConstraintEntity.RINGER_MODE_SILENT -> + ConstraintData.RingerMode(SystemRingerMode.SILENT) ConstraintEntity.CHARGING -> ConstraintData.Charging ConstraintEntity.DISCHARGING -> ConstraintData.Discharging @@ -689,15 +691,15 @@ object ConstraintEntityMapper { ) is ConstraintData.RingerMode -> when (constraint.data.ringerMode) { - io.github.sds100.keymapper.system.volume.RingerMode.NORMAL -> ConstraintEntity( + SystemRingerMode.NORMAL -> ConstraintEntity( uid = constraint.uid, ConstraintEntity.RINGER_MODE_NORMAL, ) - io.github.sds100.keymapper.system.volume.RingerMode.VIBRATE -> ConstraintEntity( + SystemRingerMode.VIBRATE -> ConstraintEntity( uid = constraint.uid, ConstraintEntity.RINGER_MODE_VIBRATE, ) - io.github.sds100.keymapper.system.volume.RingerMode.SILENT -> ConstraintEntity( + SystemRingerMode.SILENT -> ConstraintEntity( uid = constraint.uid, ConstraintEntity.RINGER_MODE_SILENT, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index 7bb26b672f..384490b13b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -17,8 +17,8 @@ import androidx.compose.material.icons.outlined.LockOpen import androidx.compose.material.icons.outlined.MobileOff import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.NotificationsOff -import androidx.compose.material.icons.outlined.Vibration import androidx.compose.material.icons.outlined.PlayArrow +import androidx.compose.material.icons.outlined.Vibration import androidx.compose.material.icons.outlined.RingVolume import androidx.compose.material.icons.outlined.ScreenLockPortrait import androidx.compose.material.icons.outlined.SignalWifiStatusbarNull diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt index a48843773b..ffbd85c775 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt @@ -112,6 +112,7 @@ class KeyMapConstraintsComparator( is ConstraintData.OrientationLandscape -> Success("") is ConstraintData.OrientationPortrait -> Success("") is ConstraintData.PhoneRinging -> Success("") + is ConstraintData.RingerMode -> Success("") is ConstraintData.ScreenOff -> Success("") is ConstraintData.ScreenOn -> Success("") is ConstraintData.WifiConnected -> if (constraint.data.ssid == null) { diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt index 2548902c5e..4af85213c7 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt @@ -1,5 +1,8 @@ package io.github.sds100.keymapper.system.volume +import kotlinx.serialization.Serializable + +@Serializable enum class RingerMode { NORMAL, VIBRATE, From f08ac663f06ebf04913ccbe5a7fdd971cd47a0f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 12:42:31 +0000 Subject: [PATCH 07/24] #2163 style: fix import ordering in ConstraintUtils --- .../github/sds100/keymapper/base/constraints/ConstraintUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index 384490b13b..048854c7b6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -18,7 +18,6 @@ import androidx.compose.material.icons.outlined.MobileOff import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.NotificationsOff import androidx.compose.material.icons.outlined.PlayArrow -import androidx.compose.material.icons.outlined.Vibration import androidx.compose.material.icons.outlined.RingVolume import androidx.compose.material.icons.outlined.ScreenLockPortrait import androidx.compose.material.icons.outlined.SignalWifiStatusbarNull @@ -26,6 +25,7 @@ import androidx.compose.material.icons.outlined.StayCurrentLandscape import androidx.compose.material.icons.outlined.StayCurrentPortrait import androidx.compose.material.icons.outlined.StopCircle import androidx.compose.material.icons.outlined.Timer +import androidx.compose.material.icons.outlined.Vibration import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material.icons.outlined.WifiOff import androidx.compose.material.icons.rounded.Android From 56899ee7fbb8cb4689a12e0b2822cf763489b0ef Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 12:46:56 +0000 Subject: [PATCH 08/24] #2163 fix: use data.ringerMode not constraint.ringerMode in when branches --- .../sds100/keymapper/base/constraints/ConstraintSnapshot.kt | 2 +- .../sds100/keymapper/base/constraints/ConstraintUiHelper.kt | 2 +- .../sds100/keymapper/base/utils/TestConstraintSnapshot.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt index 822bc32361..b568952133 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt @@ -169,7 +169,7 @@ class LazyConstraintSnapshot( callState == CallState.RINGING || audioVolumeStreams.contains(AudioManager.STREAM_RING) - is ConstraintData.RingerMode -> ringerMode == constraint.ringerMode + is ConstraintData.RingerMode -> ringerMode == constraint.data.ringerMode is ConstraintData.Charging -> isCharging is ConstraintData.Discharging -> !isCharging diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt index 039e922691..14fd7ed7a1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt @@ -164,7 +164,7 @@ class ConstraintUiHelper( is ConstraintData.InPhoneCall -> getString(R.string.constraint_in_phone_call) is ConstraintData.NotInPhoneCall -> getString(R.string.constraint_not_in_phone_call) is ConstraintData.PhoneRinging -> getString(R.string.constraint_phone_ringing) - is ConstraintData.RingerMode -> when (constraint.ringerMode) { + is ConstraintData.RingerMode -> when (constraint.data.ringerMode) { RingerMode.NORMAL -> getString(R.string.constraint_ringer_mode_normal) RingerMode.VIBRATE -> getString(R.string.constraint_ringer_mode_vibrate) RingerMode.SILENT -> getString(R.string.constraint_ringer_mode_silent) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt index 91598bb35e..89748d53df 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt @@ -109,7 +109,7 @@ class TestConstraintSnapshot( is ConstraintData.InPhoneCall -> callState == CallState.IN_PHONE_CALL is ConstraintData.NotInPhoneCall -> callState == CallState.NONE is ConstraintData.PhoneRinging -> callState == CallState.RINGING - is ConstraintData.RingerMode -> ringerMode == constraint.ringerMode + is ConstraintData.RingerMode -> ringerMode == data.ringerMode is ConstraintData.Charging -> isCharging is ConstraintData.Discharging -> !isCharging is ConstraintData.LockScreenShowing -> isLockscreenShowing From e8292f83623ed20adbda437d060312367b88ea14 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 9 Jul 2026 11:24:53 +0200 Subject: [PATCH 09/24] fix bugs with do not remap default setting --- .../keymaps/GetDefaultKeyMapOptionsUseCase.kt | 18 ++++++++++-------- .../settings/DefaultOptionsSettingsScreen.kt | 3 +++ .../base/trigger/ConfigTriggerDelegate.kt | 9 +++++---- .../base/trigger/ConfigTriggerUseCase.kt | 6 ++++-- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt index 24d8fb2034..5b01513284 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt @@ -20,46 +20,48 @@ class GetDefaultKeyMapOptionsUseCaseImpl @Inject constructor( override val defaultRepeatDelay: StateFlow = preferenceRepository.get(Keys.defaultRepeatDelay) .map { it ?: PreferenceDefaults.REPEAT_DELAY } - .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.REPEAT_DELAY) + .stateIn(coroutineScope, SharingStarted.Eagerly, PreferenceDefaults.REPEAT_DELAY) override val defaultRepeatRate: StateFlow = preferenceRepository.get(Keys.defaultRepeatRate) .map { it ?: PreferenceDefaults.REPEAT_RATE } - .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.REPEAT_RATE) + .stateIn(coroutineScope, SharingStarted.Eagerly, PreferenceDefaults.REPEAT_RATE) override val defaultHoldDownDuration: StateFlow = preferenceRepository.get(Keys.defaultHoldDownDuration) .map { it ?: PreferenceDefaults.HOLD_DOWN_DURATION } - .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.HOLD_DOWN_DURATION) + .stateIn(coroutineScope, SharingStarted.Eagerly, PreferenceDefaults.HOLD_DOWN_DURATION) override val defaultLongPressDelay: StateFlow = preferenceRepository.get(Keys.defaultLongPressDelay) .map { it ?: PreferenceDefaults.LONG_PRESS_DELAY } - .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.LONG_PRESS_DELAY) + .stateIn(coroutineScope, SharingStarted.Eagerly, PreferenceDefaults.LONG_PRESS_DELAY) override val defaultDoublePressDelay: StateFlow = preferenceRepository.get(Keys.defaultDoublePressDelay) .map { it ?: PreferenceDefaults.DOUBLE_PRESS_DELAY } - .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.DOUBLE_PRESS_DELAY) + .stateIn(coroutineScope, SharingStarted.Eagerly, PreferenceDefaults.DOUBLE_PRESS_DELAY) override val defaultSequenceTriggerTimeout: StateFlow = preferenceRepository.get(Keys.defaultSequenceTriggerTimeout) .map { it ?: PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT } .stateIn( coroutineScope, - SharingStarted.Lazily, + SharingStarted.Eagerly, PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, ) override val defaultVibrateDuration: StateFlow = preferenceRepository.get(Keys.defaultVibrateDuration) .map { it ?: PreferenceDefaults.VIBRATION_DURATION } - .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.VIBRATION_DURATION) + .stateIn(coroutineScope, SharingStarted.Eagerly, PreferenceDefaults.VIBRATION_DURATION) override val defaultDoNotRemap: StateFlow = preferenceRepository.get(Keys.defaultDoNotRemap) .map { it ?: PreferenceDefaults.DO_NOT_REMAP } - .stateIn(coroutineScope, SharingStarted.Lazily, PreferenceDefaults.DO_NOT_REMAP) + // Use Eagerly because otherwise this value is never updated if only .value is called + // and not collected. + .stateIn(coroutineScope, SharingStarted.Eagerly, PreferenceDefaults.DO_NOT_REMAP) } interface GetDefaultKeyMapOptionsUseCase { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt index b36d4e4fc9..a93e1516c7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt @@ -214,6 +214,9 @@ private fun Content( // Do not remap SwitchPreferenceCompose( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), title = stringResource(R.string.title_pref_default_do_not_remap), text = stringResource(R.string.summary_pref_default_do_not_remap), icon = Icons.Rounded.Keyboard, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt index 3a6d6265fb..c7c5ae89a5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -66,7 +66,7 @@ class ConfigTriggerDelegate { device: KeyEventTriggerDevice, requiresIme: Boolean, otherTriggerKeys: List = emptyList(), - defaultDoNotRemap: Boolean = false, + doNotRemap: Boolean, ): Trigger { val isPowerKey = KeyEventUtils.isPowerButtonKey(keyCode, scanCode) @@ -80,7 +80,7 @@ class ConfigTriggerDelegate { } } - var consumeKeyEvent = !defaultDoNotRemap + var consumeKeyEvent = !doNotRemap // Issue #753 if (KeyEventUtils.isModifierKey(keyCode)) { @@ -131,7 +131,7 @@ class ConfigTriggerDelegate { scanCode: Int, device: EvdevDeviceInfo, otherTriggerKeys: List = emptyList(), - defaultDoNotRemap: Boolean = false, + doNotRemap: Boolean = false, ): Trigger { val isPowerKey = KeyEventUtils.isPowerButtonKey(keyCode, scanCode) @@ -160,7 +160,7 @@ class ConfigTriggerDelegate { scanCode = scanCode, device = device, clickType = clickType, - consumeEvent = !defaultDoNotRemap, + consumeEvent = !doNotRemap, detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty(), ) @@ -188,6 +188,7 @@ class ConfigTriggerDelegate { val newMode = when { trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined /* Automatically make it a parallel trigger when the user makes a trigger with more than one key diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt index e24483c034..1b348dc47e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -63,7 +63,9 @@ class ConfigTriggerUseCaseImpl @Inject constructor( .mapNotNull { key -> when (key) { is EvdevTriggerKeyEntity -> EvdevTriggerKey.fromEntity(key) + is KeyEventTriggerKeyEntity -> KeyEventTriggerKey.fromEntity(key) + is AssistantTriggerKeyEntity, is FingerprintTriggerKeyEntity, is FloatingButtonKeyEntity, @@ -118,7 +120,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( device, requiresIme, otherTriggerKeys = otherTriggerKeys, - defaultDoNotRemap = defaultDoNotRemap.value, + doNotRemap = defaultDoNotRemap.value, ) } @@ -130,7 +132,7 @@ class ConfigTriggerUseCaseImpl @Inject constructor( scanCode, device, otherTriggerKeys = otherTriggerKeys, - defaultDoNotRemap = defaultDoNotRemap.value, + doNotRemap = defaultDoNotRemap.value, ) } From 7ad06237b6d02564355577f1d4602e5d462b3e52 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 9 Jul 2026 11:31:49 +0200 Subject: [PATCH 10/24] bump version to 4.3.0 --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index eb15c83b69..75456f40c1 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.2.2 +VERSION_NAME=4.3.0 VERSION_CODE=257 From dd4c15cb656dbe1d9a36c953e1e9032228d1c325 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 9 Jul 2026 11:38:11 +0200 Subject: [PATCH 11/24] fix changelog --- CHANGELOG.md | 929 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 619 insertions(+), 310 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4d637bbf0..5fba1b2f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,34 +1,45 @@ +## [4.3.0](https://github.com/sds100/KeyMapper/releases/tag/v4.3.0) + +#### TO BE RELEASED + +## Added + +- #2163 Add ringer mode constraints (Ring, Vibrate, Silent). +- #2140 Add monochrome app icon layer for themed icon support on Android 13+. + ## [4.2.1](https://github.com/sds100/KeyMapper/releases/tag/v4.2.1) #### 09 June 2026 ## Fixed -- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged. -- #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there. -- #2153 Prevent Direct Boot startup from initializing credential-encrypted app storage before the user unlocks. -- #2157 The "choose setting" screen now uses `settings list` via the system bridge when expert mode is active, surfacing all device settings instead of only those visible through the ContentProvider. +- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been + acknowledged. +- #2156 Do not throw an error when the Talkback application can not be found because there are many + different package names out there. +- #2153 Prevent Direct Boot startup from initializing credential-encrypted app storage before the + user unlocks. +- #2157 The "choose setting" screen now uses `settings list` via the system bridge when expert mode + is active, surfacing all device settings instead of only those visible through the + ContentProvider. ## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0) #### 03 June 2026 -## Added - -- #2163 Add ringer mode constraints (Ring, Vibrate, Silent). -- #2140 Add monochrome app icon layer for themed icon support on Android 13+. - ## Fixed -- #2074 Scrolling the action or trigger list no longer accidentally moves items; reordering by drag now only activates from the drag handle or via long-press. +- #2074 Scrolling the action or trigger list no longer accidentally moves items; reordering by drag + now only activates from the drag handle or via long-press. ## Changed -- #1369 Add content descriptions to drag handles and custom "Move up"/"Move down" accessibility actions for trigger and action list items, improving TalkBack support for reordering. -- #262 Add "TalkBack gesture" action to simulate TalkBack navigation gestures (swipes, multi-finger taps, and multi-directional swipes). +- #1369 Add content descriptions to drag handles and custom "Move up"/"Move down" accessibility + actions for trigger and action list items, improving TalkBack support for reordering. +- #262 Add "TalkBack gesture" action to simulate TalkBack navigation gestures (swipes, multi-finger + taps, and multi-directional swipes). - #2076 Use "any input device" as the default for triggers. - ## [4.1.0](https://github.com/sds100/KeyMapper/releases/tag/v4.1.0) #### 10 May 2026 @@ -37,11 +48,13 @@ - #2067 add action to select all text in the focused field. - #2045 add action to input on-screen keyboard enter/send button. -- #2106 disable the keyboard auto-switching setting when manually switching the keyboard in the Key Mapper homescreen menu. +- #2106 disable the keyboard auto-switching setting when manually switching the keyboard in the Key + Mapper homescreen menu. - #1029 add action to show a toast message. - #2081 add getevent debug screen. - #2087 small segmented button text is not readable in dark mode. -- #2077 rename "This device" and "any device" to "This Android device" and "Any input device" to prevent confusion. +- #2077 rename "This device" and "any device" to "This Android device" and "Any input device" to + prevent confusion. ## Changed @@ -79,7 +92,8 @@ - #2030 do not filter out unknown evdev key events. - #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. +- #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) @@ -87,8 +101,10 @@ ## Fixed -- [#2006](https://github.com/keymapperorg/KeyMapper/issues/2006) Actually fixed the bug with crash with Locale switching -- [#2014](https://github.com/keymapperorg/KeyMapper/issues/2014) Reduce mouse latency when remapped with Expert mode. +- [#2006](https://github.com/keymapperorg/KeyMapper/issues/2006) Actually fixed the bug with crash + with Locale switching +- [#2014](https://github.com/keymapperorg/KeyMapper/issues/2014) Reduce mouse latency when remapped + with Expert mode. ## [4.0.2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.2) @@ -100,18 +116,21 @@ ## Removed -- [#1973](https://github.com/keymapperorg/KeyMapper/issues/1973) [#2006](https://github.com/keymapperorg/KeyMapper/issues/2006) Removed language switching feature due to crash with no solution. +- [#1973](https://github.com/keymapperorg/KeyMapper/issues/1973) [#2006](https://github.com/keymapperorg/KeyMapper/issues/2006) + Removed language switching feature due to crash with no solution. ## [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. +- #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) @@ -126,25 +145,39 @@ See the changes in the previous beta releases for everything new. There is _a lo ## Added -- [#1970](https://github.com/keymapperorg/KeyMapper/issues/1970) dynamically build the key code list so key codes in new Android releases are automatically +- [#1970](https://github.com/keymapperorg/KeyMapper/issues/1970) dynamically build the key code list + so key codes in new Android releases are automatically included. -- [#1939](https://github.com/keymapperorg/KeyMapper/issues/1939) show notification when Expert Mode fails to start due to be being disconnected from WiFi. -- [#1973](https://github.com/keymapperorg/KeyMapper/issues/1973) add setting to change app language on Android 13+. +- [#1939](https://github.com/keymapperorg/KeyMapper/issues/1939) show notification when Expert Mode + fails to start due to be being disconnected from WiFi. +- [#1973](https://github.com/keymapperorg/KeyMapper/issues/1973) add setting to change app language + on Android 13+. ## Fixed -- [#1986](https://github.com/keymapperorg/KeyMapper/issues/1986) trigger screen is usable on slightly rectangular screens with a low DPI. +- [#1986](https://github.com/keymapperorg/KeyMapper/issues/1986) trigger screen is usable on + slightly rectangular screens with a low DPI. - [#1972](https://github.com/keymapperorg/KeyMapper/issues/1972) Expert Mode works on Android 10. -- [#1976](https://github.com/keymapperorg/KeyMapper/issues/1976) Panic in Rust system bridge code on some devices. -- [#1971](https://github.com/keymapperorg/KeyMapper/issues/1971) Media actions work again in some apps, like YouTube. -- [#1961](https://github.com/keymapperorg/KeyMapper/issues/1961) Disabling setup assistant shows a notification asking for pairing code immediately. -- [#1983](https://github.com/keymapperorg/KeyMapper/issues/1983) Inputting a modifier key and another key as actions through Expert mode applies the correct key character map. -- [#1990](https://github.com/keymapperorg/KeyMapper/issues/1990) Passthrough the device id of the trigger to the key event action if one is not manually specified -- [#1982](https://github.com/keymapperorg/KeyMapper/issues/1982) Text action does not need Key Mapper input method on Android 13+. -- [#1989](https://github.com/keymapperorg/KeyMapper/issues/1989) center the "Trigger and actions" and "Constraint and more" tabs. -- [#1392](https://github.com/keymapperorg/KeyMapper/issues/1392) Add action to enable/disable/toggle night shift. -- [#1675](https://github.com/keymapperorg/KeyMapper/issues/1675) Option to make floating buttons movable. -- [#1949](https://github.com/keymapperorg/KeyMapper/issues/1949) Floating buttons are completely invisible when pressed if background and border opacity is set to 0. +- [#1976](https://github.com/keymapperorg/KeyMapper/issues/1976) Panic in Rust system bridge code on + some devices. +- [#1971](https://github.com/keymapperorg/KeyMapper/issues/1971) Media actions work again in some + apps, like YouTube. +- [#1961](https://github.com/keymapperorg/KeyMapper/issues/1961) Disabling setup assistant shows a + notification asking for pairing code immediately. +- [#1983](https://github.com/keymapperorg/KeyMapper/issues/1983) Inputting a modifier key and + another key as actions through Expert mode applies the correct key character map. +- [#1990](https://github.com/keymapperorg/KeyMapper/issues/1990) Passthrough the device id of the + trigger to the key event action if one is not manually specified +- [#1982](https://github.com/keymapperorg/KeyMapper/issues/1982) Text action does not need Key + Mapper input method on Android 13+. +- [#1989](https://github.com/keymapperorg/KeyMapper/issues/1989) center the "Trigger and actions" + and "Constraint and more" tabs. +- [#1392](https://github.com/keymapperorg/KeyMapper/issues/1392) Add action to enable/disable/toggle + night shift. +- [#1675](https://github.com/keymapperorg/KeyMapper/issues/1675) Option to make floating buttons + movable. +- [#1949](https://github.com/keymapperorg/KeyMapper/issues/1949) Floating buttons are completely + invisible when pressed if background and border opacity is set to 0. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) @@ -152,14 +185,18 @@ See the changes in the previous beta releases for everything new. There is _a lo ## Added -- [#1964](https://github.com/keymapperorg/KeyMapper/issues/1964) show the command to start Expert Mode with a shell command. +- [#1964](https://github.com/keymapperorg/KeyMapper/issues/1964) show the command to start Expert + Mode with a shell command. ## Bug fixes -- [#1968](https://github.com/keymapperorg/KeyMapper/issues/1968) Device controls action no longer works on Android 16+ so it has been disabled on new Android +- [#1968](https://github.com/keymapperorg/KeyMapper/issues/1968) Device controls action no longer + works on Android 16+ so it has been disabled on new Android versions. -- [#1967](https://github.com/keymapperorg/KeyMapper/issues/1967) Still start system bridge if granting WRITE_SECURE_SETTINGS fails. -- [#1965](https://github.com/keymapperorg/KeyMapper/issues/1965) Better system bridge support on Xiaomi devices and ask to enable "USB debugging security +- [#1967](https://github.com/keymapperorg/KeyMapper/issues/1967) Still start system bridge if + granting WRITE_SECURE_SETTINGS fails. +- [#1965](https://github.com/keymapperorg/KeyMapper/issues/1965) Better system bridge support on + Xiaomi devices and ask to enable "USB debugging security settings" in developer options. ## [4.0.0 Beta 5](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.05) @@ -170,12 +207,15 @@ Happy new year! ## Added -- [#1947](https://github.com/keymapperorg/KeyMapper/issues/1947) show tip to use expert mode where the old option for screen off remapping used to be +- [#1947](https://github.com/keymapperorg/KeyMapper/issues/1947) show tip to use expert mode where + the old option for screen off remapping used to be ## Bug fixes -- [#1955](https://github.com/keymapperorg/KeyMapper/issues/1955) step forward and step backward media actions support more apps. -- [#1940](https://github.com/keymapperorg/KeyMapper/issues/1940) improve reliability of clicking pairing code button in Wireless Debugging settings. +- [#1955](https://github.com/keymapperorg/KeyMapper/issues/1955) step forward and step backward + media actions support more apps. +- [#1940](https://github.com/keymapperorg/KeyMapper/issues/1940) improve reliability of clicking + pairing code button in Wireless Debugging settings. ## [4.0.0 Beta 4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.04) @@ -188,21 +228,32 @@ free. ## Added -- [#1915](https://github.com/keymapperorg/KeyMapper/issues/1915) ask user to remove "adb shell" from Shell command. -- [#1904](https://github.com/keymapperorg/KeyMapper/issues/1904) inform the user how to enable the accessibility service with PRO mode or ADB. -- [#1911](https://github.com/keymapperorg/KeyMapper/issues/1911) constraint for physical device orientation that ignores auto rotate setting. -- [#1918](https://github.com/keymapperorg/KeyMapper/issues/1918) improve how key event actions are performed with system bridge. -- [#1905](https://github.com/keymapperorg/KeyMapper/issues/1905) system bridge log is now visible in Key Mapper log. -- [#1941](https://github.com/keymapperorg/KeyMapper/issues/1941) show loading indicator when starting system bridge. +- [#1915](https://github.com/keymapperorg/KeyMapper/issues/1915) ask user to remove "adb shell" from + Shell command. +- [#1904](https://github.com/keymapperorg/KeyMapper/issues/1904) inform the user how to enable the + accessibility service with PRO mode or ADB. +- [#1911](https://github.com/keymapperorg/KeyMapper/issues/1911) constraint for physical device + orientation that ignores auto rotate setting. +- [#1918](https://github.com/keymapperorg/KeyMapper/issues/1918) improve how key event actions are + performed with system bridge. +- [#1905](https://github.com/keymapperorg/KeyMapper/issues/1905) system bridge log is now visible in + Key Mapper log. +- [#1941](https://github.com/keymapperorg/KeyMapper/issues/1941) show loading indicator when + starting system bridge. ## Bug fixes -- [#1913](https://github.com/keymapperorg/KeyMapper/issues/1913) actually save the option to detect with scan code -- [#1931](https://github.com/keymapperorg/KeyMapper/issues/1931) fix Close and Remove From Recents action on some Android 13 revisions -- [#1926](https://github.com/keymapperorg/KeyMapper/issues/1926) PRO mode triggers for external devices work when the device reconnects. -- [#1918](https://github.com/keymapperorg/KeyMapper/issues/1918) PRO mode key maps can input key codes that aren't originally supported by the trigger +- [#1913](https://github.com/keymapperorg/KeyMapper/issues/1913) actually save the option to detect + with scan code +- [#1931](https://github.com/keymapperorg/KeyMapper/issues/1931) fix Close and Remove From Recents + action on some Android 13 revisions +- [#1926](https://github.com/keymapperorg/KeyMapper/issues/1926) PRO mode triggers for external + devices work when the device reconnects. +- [#1918](https://github.com/keymapperorg/KeyMapper/issues/1918) PRO mode key maps can input key + codes that aren't originally supported by the trigger device. -- [#1934](https://github.com/keymapperorg/KeyMapper/issues/1934) hold down option for Tap Screen action is added back. +- [#1934](https://github.com/keymapperorg/KeyMapper/issues/1934) hold down option for Tap Screen + action is added back. - Log less verbose. ## [4.0.0 Beta 3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.03) @@ -211,18 +262,27 @@ free. ## Added -- [#1871](https://github.com/keymapperorg/KeyMapper/issues/1871) action to modify any system settings. -- [#1221](https://github.com/keymapperorg/KeyMapper/issues/1221) action to show a custom notification. -- [#1491](https://github.com/keymapperorg/KeyMapper/issues/1491) action to toggle/enable/disable hotspot. -- [#1414](https://github.com/keymapperorg/KeyMapper/issues/1414) constraint for when the keyboard is showing. -- [#1900](https://github.com/keymapperorg/KeyMapper/issues/1900) log to logcat if extra logging is enabled. -- [#1902](https://github.com/keymapperorg/KeyMapper/issues/1902) add toggle next to record trigger button to use PRO mode. -- [#1909](https://github.com/keymapperorg/KeyMapper/issues/1909) categorise constraints similar to actions. +- [#1871](https://github.com/keymapperorg/KeyMapper/issues/1871) action to modify any system + settings. +- [#1221](https://github.com/keymapperorg/KeyMapper/issues/1221) action to show a custom + notification. +- [#1491](https://github.com/keymapperorg/KeyMapper/issues/1491) action to toggle/enable/disable + hotspot. +- [#1414](https://github.com/keymapperorg/KeyMapper/issues/1414) constraint for when the keyboard is + showing. +- [#1900](https://github.com/keymapperorg/KeyMapper/issues/1900) log to logcat if extra logging is + enabled. +- [#1902](https://github.com/keymapperorg/KeyMapper/issues/1902) add toggle next to record trigger + button to use PRO mode. +- [#1909](https://github.com/keymapperorg/KeyMapper/issues/1909) categorise constraints similar to + actions. ## Bug fixes -- [#1901](https://github.com/keymapperorg/KeyMapper/issues/1901) prompt user to set default USB configuration to 'No data transfer' after starting pro mode. -- [#1898](https://github.com/keymapperorg/KeyMapper/issues/1898) do not launch directly into the Wireless Debugging activity on Xiaomi devices due to a bug +- [#1901](https://github.com/keymapperorg/KeyMapper/issues/1901) prompt user to set default USB + configuration to 'No data transfer' after starting pro mode. +- [#1898](https://github.com/keymapperorg/KeyMapper/issues/1898) do not launch directly into the + Wireless Debugging activity on Xiaomi devices due to a bug they introduced. ## [4.0.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.02) @@ -231,7 +291,8 @@ free. ## Added -- [#1890](https://github.com/keymapperorg/KeyMapper/issues/1890) add button to save log to file and share it. The clipboard button now cuts off older entries +- [#1890](https://github.com/keymapperorg/KeyMapper/issues/1890) add button to save log to file and + share it. The clipboard button now cuts off older entries and keeps newest ones. ## Fixed @@ -240,7 +301,8 @@ free. method with Wireless Debugging and WRITE_SECURE_SETTINGS permission. - Starting system bridge for the first time would be janky because granting READ_LOGS kills the app process. Only grant for READ_LOGS when sharing logcat from settings. -- [#1886](https://github.com/keymapperorg/KeyMapper/issues/1886) mobile data actions work in PRO mode. +- [#1886](https://github.com/keymapperorg/KeyMapper/issues/1886) mobile data actions work in PRO + mode. ## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01) @@ -248,22 +310,32 @@ free. ## Added -- [#761](https://github.com/keymapperorg/KeyMapper/issues/761) Detect keys with scancodes. Key Mapper will do this automatically if the key code is unknown +- [#761](https://github.com/keymapperorg/KeyMapper/issues/761) Detect keys with scancodes. Key + Mapper will do this automatically if the key code is unknown or you record different physical keys from the same device with the same key code. - Redesign the Settings screen. - Shortcuts on the trigger screen that guide you how to set up different types of buttons. -- [#1788](https://github.com/keymapperorg/KeyMapper/issues/1788) dismiss lockscreen when launching app action from lockscreen +- [#1788](https://github.com/keymapperorg/KeyMapper/issues/1788) dismiss lockscreen when launching + app action from lockscreen - Show tips for parallel and sequence triggers, and constraints in the trigger screen -- [#397](https://github.com/keymapperorg/KeyMapper/issues/397) enable/disable all key maps in a group -- [#1773](https://github.com/keymapperorg/KeyMapper/issues/1773) Option to show floating buttons on top of keyboard or notification panel. -- [#1335](https://github.com/keymapperorg/KeyMapper/issues/1335) Intent API to enable/disable/toggle a key map. -- [#114](https://github.com/keymapperorg/KeyMapper/issues/114) action to force stop app, and an action to clear an app from recents -- [#727](https://github.com/keymapperorg/KeyMapper/issues/727) Actions to send SMS messages: "Send SMS" and "Compose SMS" -- [#1819](https://github.com/keymapperorg/KeyMapper/issues/1819) Explain how to enable the accessibility service restricted setting +- [#397](https://github.com/keymapperorg/KeyMapper/issues/397) enable/disable all key maps in a + group +- [#1773](https://github.com/keymapperorg/KeyMapper/issues/1773) Option to show floating buttons on + top of keyboard or notification panel. +- [#1335](https://github.com/keymapperorg/KeyMapper/issues/1335) Intent API to enable/disable/toggle + a key map. +- [#114](https://github.com/keymapperorg/KeyMapper/issues/114) action to force stop app, and an + action to clear an app from recents +- [#727](https://github.com/keymapperorg/KeyMapper/issues/727) Actions to send SMS messages: "Send + SMS" and "Compose SMS" +- [#1819](https://github.com/keymapperorg/KeyMapper/issues/1819) Explain how to enable the + accessibility service restricted setting - [#661](https://github.com/keymapperorg/KeyMapper/issues/661) Action to execute shell commands. -- [#991](https://github.com/keymapperorg/KeyMapper/issues/991) Consolidated volume and stream actions. +- [#991](https://github.com/keymapperorg/KeyMapper/issues/991) Consolidated volume and stream + actions. - [#1066](https://github.com/keymapperorg/KeyMapper/issues/1066) Action to mute/unmute microphone. -- [#985](https://github.com/keymapperorg/KeyMapper/issues/985) Constraints for foldable hinge being open/closed. +- [#985](https://github.com/keymapperorg/KeyMapper/issues/985) Constraints for foldable hinge being + open/closed. ## Removed @@ -281,16 +353,20 @@ free. - Restoring subgroups works and does not freeze Key Mapper. - Do not show duplicate constraint shortcuts. - Make WiFi connected constraints more reliable -- [#1818](https://github.com/keymapperorg/KeyMapper/issues/1818) auto switching of the Key Mapper keyboard when typing is more reliable and quicker on +- [#1818](https://github.com/keymapperorg/KeyMapper/issues/1818) auto switching of the Key Mapper + keyboard when typing is more reliable and quicker on Android 13+ -- [#1818](https://github.com/keymapperorg/KeyMapper/issues/1818) auto switching of the Key Mapper keyboard now requires Android 11+. On older versions it was +- [#1818](https://github.com/keymapperorg/KeyMapper/issues/1818) auto switching of the Key Mapper + keyboard now requires Android 11+. On older versions it was only possible with WRITE_SECURE_SETTINGS but very few users are on these old Android versions so it is not worth the extra maintenance effort. -- [#1818](https://github.com/keymapperorg/KeyMapper/issues/1818) the Key Mapper GUI Keyboard is no longer mentioned in the app. It still works but PRO mode +- [#1818](https://github.com/keymapperorg/KeyMapper/issues/1818) the Key Mapper GUI Keyboard is no + longer mentioned in the app. It still works but PRO mode and the auto switching feature are the preferred way to work around the limitations of the Key Mapper keyboard. - Allow selecting notification and alarm sound and not just ringtones for Sound action. -- [#1064](https://github.com/keymapperorg/KeyMapper/issues/1064) wait for switch keyboard action to complete before doing next action. +- [#1064](https://github.com/keymapperorg/KeyMapper/issues/1064) wait for switch keyboard action to + complete before doing next action. ## [3.2.1](https://github.com/sds100/KeyMapper/releases/tag/v3.2.1) @@ -306,33 +382,46 @@ free. ## Added -- [#1466](https://github.com/keymapperorg/KeyMapper/issues/1466) show onboarding when creating a key map for the first time +- [#1466](https://github.com/keymapperorg/KeyMapper/issues/1466) show onboarding when creating a key + map for the first time - [#1729](https://github.com/keymapperorg/KeyMapper/issues/1729) target Android 16. -- [#1725](https://github.com/keymapperorg/KeyMapper/issues/1725) action to move cursor to previous/next character, word, line, paragraph, or page. +- [#1725](https://github.com/keymapperorg/KeyMapper/issues/1725) action to move cursor to + previous/next character, word, line, paragraph, or page. - Names for new key codes introduced in recent Android versions ## Changed -- [#1711](https://github.com/keymapperorg/KeyMapper/issues/1711) major refactoring of the entire codebase into separate Gradle modules. -- [#1701](https://github.com/keymapperorg/KeyMapper/issues/1701) improve the order of the actions and categories +- [#1711](https://github.com/keymapperorg/KeyMapper/issues/1711) major refactoring of the entire + codebase into separate Gradle modules. +- [#1701](https://github.com/keymapperorg/KeyMapper/issues/1701) improve the order of the actions + and categories - Key Mapper keyboard or Shizuku are no longer required for the action to move the cursor to the end ## Bug fixes -- [#1686](https://github.com/keymapperorg/KeyMapper/issues/1686) (more fixes) do not show some screens behind system bars on the left/right side of the +- [#1686](https://github.com/keymapperorg/KeyMapper/issues/1686) (more fixes) do not show some + screens behind system bars on the left/right side of the device. -- [#1701](https://github.com/keymapperorg/KeyMapper/issues/1701) optimize the trigger screen for smaller screens so elements are less cut off. -- [#1699](https://github.com/keymapperorg/KeyMapper/issues/1699) Do not highlight a floating button as if it is pressed after triggering a key event action +- [#1701](https://github.com/keymapperorg/KeyMapper/issues/1701) optimize the trigger screen for + smaller screens so elements are less cut off. +- [#1699](https://github.com/keymapperorg/KeyMapper/issues/1699) Do not highlight a floating button + as if it is pressed after triggering a key event action from it. - Button to copy the key map UID to the clipboard is invisible on small screens. -- [#1709](https://github.com/keymapperorg/KeyMapper/issues/1709) Quick settings tiles were causing crashes on Android 15 -- [#1714](https://github.com/keymapperorg/KeyMapper/issues/1714) Editing "interact with app element" actions works. -- [#1707](https://github.com/keymapperorg/KeyMapper/issues/1707) do not back up sound files if no key maps are using them -- [#797](https://github.com/keymapperorg/KeyMapper/issues/797) [#1719](https://github.com/keymapperorg/KeyMapper/issues/1719) execute key maps that can fix themselves. E.g having an action to select the Key Mapper +- [#1709](https://github.com/keymapperorg/KeyMapper/issues/1709) Quick settings tiles were causing + crashes on Android 15 +- [#1714](https://github.com/keymapperorg/KeyMapper/issues/1714) Editing "interact with app element" + actions works. +- [#1707](https://github.com/keymapperorg/KeyMapper/issues/1707) do not back up sound files if no + key maps are using them +- [#797](https://github.com/keymapperorg/KeyMapper/issues/797) [#1719](https://github.com/keymapperorg/KeyMapper/issues/1719) + execute key maps that can fix themselves. E.g having an action to select the Key Mapper Keyboard before a key code action. -- [#1735](https://github.com/keymapperorg/KeyMapper/issues/1735) Floating buttons no longer flash on screen when the accessibility service restarts if they +- [#1735](https://github.com/keymapperorg/KeyMapper/issues/1735) Floating buttons no longer flash on + screen when the accessibility service restarts if they are not supposed to be visible. -- [#1717](https://github.com/keymapperorg/KeyMapper/issues/1717) do not show floating buttons if quick settings is expanded on the lockscreen. +- [#1717](https://github.com/keymapperorg/KeyMapper/issues/1717) do not show floating buttons if + quick settings is expanded on the lockscreen. - Correctly show error that Airplane mode actions require root ## [3.1.1](https://github.com/sds100/KeyMapper/releases/tag/v3.1.1) @@ -341,13 +430,16 @@ free. ## Added -- [#1637](https://github.com/keymapperorg/KeyMapper/issues/1637) show a home screen error if notification permission is not granted. -- [#1435](https://github.com/keymapperorg/KeyMapper/issues/1435) Pick system sounds/ringtones for the Sound action. +- [#1637](https://github.com/keymapperorg/KeyMapper/issues/1637) show a home screen error if + notification permission is not granted. +- [#1435](https://github.com/keymapperorg/KeyMapper/issues/1435) Pick system sounds/ringtones for + the Sound action. ## Bug fixes - Do not automatically select the key mapper keyboard when the accessibility service starts. -- [#1686](https://github.com/keymapperorg/KeyMapper/issues/1686) do not show some screens behind system bars on the left/right side of the device. +- [#1686](https://github.com/keymapperorg/KeyMapper/issues/1686) do not show some screens behind + system bars on the left/right side of the device. - Use same sized list items when choosing a constraint. ## [3.1.0](https://github.com/sds100/KeyMapper/releases/tag/v3.1.0) @@ -357,9 +449,12 @@ free. ## Added - [#699](https://github.com/keymapperorg/KeyMapper/issues/699) Time constraints ⏰ -- [#257](https://github.com/keymapperorg/KeyMapper/issues/257) Action to interact with user interface elements inside other apps. -- [#1663](https://github.com/keymapperorg/KeyMapper/issues/1663) Actions to stop, step forward, and step backward playing media. -- [#1682](https://github.com/keymapperorg/KeyMapper/issues/1682) Show "Purchased!" text next to the use button for advanced triggers. +- [#257](https://github.com/keymapperorg/KeyMapper/issues/257) Action to interact with user + interface elements inside other apps. +- [#1663](https://github.com/keymapperorg/KeyMapper/issues/1663) Actions to stop, step forward, and + step backward playing media. +- [#1682](https://github.com/keymapperorg/KeyMapper/issues/1682) Show "Purchased!" text next to the + use button for advanced triggers. ## Changed @@ -367,11 +462,14 @@ free. ## Bug fixes -- [#1683](https://github.com/keymapperorg/KeyMapper/issues/1683) key event actions work in Minecraft and other apps again. +- [#1683](https://github.com/keymapperorg/KeyMapper/issues/1683) key event actions work in Minecraft + and other apps again. - Export log files as .txt instead of .zip files. -- [#1684](https://github.com/keymapperorg/KeyMapper/issues/1684) Removed the redundant and broken refresh devices button when configuring a key event action +- [#1684](https://github.com/keymapperorg/KeyMapper/issues/1684) Removed the redundant and broken + refresh devices button when configuring a key event action because they are automatically refreshed anyway. -- [#1687](https://github.com/keymapperorg/KeyMapper/issues/1687) restoring key map groups would sometimes fail. +- [#1687](https://github.com/keymapperorg/KeyMapper/issues/1687) restoring key map groups would + sometimes fail. ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) @@ -379,15 +477,20 @@ free. ## Added -- [#1652](https://github.com/keymapperorg/KeyMapper/issues/1652) Bring back the menu button to show input method picker. -- [#1657](https://github.com/keymapperorg/KeyMapper/issues/1657) Turn on repeat by default for volume actions. +- [#1652](https://github.com/keymapperorg/KeyMapper/issues/1652) Bring back the menu button to show + input method picker. +- [#1657](https://github.com/keymapperorg/KeyMapper/issues/1657) Turn on repeat by default for + volume actions. ## Changed -- [#1654](https://github.com/keymapperorg/KeyMapper/issues/1654) The Key Mapper keyboard is now required again for Text actions because the accessibility +- [#1654](https://github.com/keymapperorg/KeyMapper/issues/1654) The Key Mapper keyboard is now + required again for Text actions because the accessibility service API does not work in all situations. -- [#1653](https://github.com/keymapperorg/KeyMapper/issues/1653) Hide the export/import menu buttons in groups. -- [#1553](https://github.com/keymapperorg/KeyMapper/issues/1553) Hide double press option for side key and fingerprint gesture triggers because it is +- [#1653](https://github.com/keymapperorg/KeyMapper/issues/1653) Hide the export/import menu buttons + in groups. +- [#1553](https://github.com/keymapperorg/KeyMapper/issues/1553) Hide double press option for side + key and fingerprint gesture triggers because it is misleading. Double activations can be done with sequence triggers instead. - [#1669](https://github.com/keymapperorg/KeyMapper/issues/1669) Change quick settings tile text. @@ -395,15 +498,23 @@ free. - Inputting key events with Shizuku does not crash the app if a Key Mapper keyboard is being used at the same time. And latency when inputting key events has been improved in some apps. -- [#1646](https://github.com/keymapperorg/KeyMapper/issues/1646) disabling Bluetooth clears the list of connected devices. -- [#1655](https://github.com/keymapperorg/KeyMapper/issues/1655) do not crash when restoring key map groups. -- [#1649](https://github.com/keymapperorg/KeyMapper/issues/1649) show purchase verification failed error if no network connection. -- [#1648](https://github.com/keymapperorg/KeyMapper/issues/1648) caching purchases works so you can use floating buttons and assistant trigger without an +- [#1646](https://github.com/keymapperorg/KeyMapper/issues/1646) disabling Bluetooth clears the list + of connected devices. +- [#1655](https://github.com/keymapperorg/KeyMapper/issues/1655) do not crash when restoring key map + groups. +- [#1649](https://github.com/keymapperorg/KeyMapper/issues/1649) show purchase verification failed + error if no network connection. +- [#1648](https://github.com/keymapperorg/KeyMapper/issues/1648) caching purchases works so you can + use floating buttons and assistant trigger without an internet connection. -- [#1658](https://github.com/keymapperorg/KeyMapper/issues/1658) floating buttons appear in the wrong place in portrait if saved in landscape. -- [#1659](https://github.com/keymapperorg/KeyMapper/issues/1659) Use trigger does not work if the screen orientation changes when re-entering the app. -- [#1668](https://github.com/keymapperorg/KeyMapper/issues/1668) Crashes when floating menu does not fit in the display height. -- [#1667](https://github.com/keymapperorg/KeyMapper/issues/1667) Hold down mode UI is missing from 2.8. +- [#1658](https://github.com/keymapperorg/KeyMapper/issues/1658) floating buttons appear in the + wrong place in portrait if saved in landscape. +- [#1659](https://github.com/keymapperorg/KeyMapper/issues/1659) Use trigger does not work if the + screen orientation changes when re-entering the app. +- [#1668](https://github.com/keymapperorg/KeyMapper/issues/1668) Crashes when floating menu does not + fit in the display height. +- [#1667](https://github.com/keymapperorg/KeyMapper/issues/1667) Hold down mode UI is missing from + 2.8. ## [3.0.0](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0) @@ -411,7 +522,8 @@ _See the changes from previous 3.0 Beta releases._ #### 10 April 2025 -- [#1635](https://github.com/keymapperorg/KeyMapper/issues/1635) do not crash if the URL for the HTTP action is malformed +- [#1635](https://github.com/keymapperorg/KeyMapper/issues/1635) do not crash if the URL for the + HTTP action is malformed ## [3.0 Beta 5](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.5) @@ -423,7 +535,8 @@ _See the changes from previous 3.0 Beta releases as well._ ## Bug fixes -- [#1627](https://github.com/keymapperorg/KeyMapper/issues/1627) open camera app action does not work when device is locked +- [#1627](https://github.com/keymapperorg/KeyMapper/issues/1627) open camera app action does not + work when device is locked ## [3.0 Beta 4](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.4) @@ -433,8 +546,10 @@ _See the changes from previous 3.0 Beta releases as well._ ## Added -- [#1620](https://github.com/keymapperorg/KeyMapper/issues/1620) enable Key Mapper Basic Input Method without user interaction on Android 13+. -- [#1619](https://github.com/keymapperorg/KeyMapper/issues/1619) Automatically select the non key mapper keyboard when the device is locked and wanting to +- [#1620](https://github.com/keymapperorg/KeyMapper/issues/1620) enable Key Mapper Basic Input + Method without user interaction on Android 13+. +- [#1619](https://github.com/keymapperorg/KeyMapper/issues/1619) Automatically select the non key + mapper keyboard when the device is locked and wanting to type. ## Changed @@ -443,7 +558,8 @@ _See the changes from previous 3.0 Beta releases as well._ ## Bug fixes -- [#1618](https://github.com/keymapperorg/KeyMapper/issues/1618), [#1532](https://github.com/keymapperorg/KeyMapper/issues/1532), [#1590](https://github.com/keymapperorg/KeyMapper/issues/1590) The Key Mapper keyboard is no longer required for Text actions. +- [#1618](https://github.com/keymapperorg/KeyMapper/issues/1618), [#1532](https://github.com/keymapperorg/KeyMapper/issues/1532), [#1590](https://github.com/keymapperorg/KeyMapper/issues/1590) + The Key Mapper keyboard is no longer required for Text actions. - Flashlight action works again on devices that do not support variable brightness ## [3.0 Beta 3](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.3) @@ -456,10 +572,13 @@ This is not an April Fool's joke ;) ## Added -- [#320](https://github.com/keymapperorg/KeyMapper/issues/320) 🗂️ Key map groups! You can now sort key maps into groups and share constraints across all the +- [#320](https://github.com/keymapperorg/KeyMapper/issues/320) 🗂️ Key map groups! You can now sort + key maps into groups and share constraints across all the key maps in the group. -- [#1586](https://github.com/keymapperorg/KeyMapper/issues/1586) 🎨 Customise floating button border and background opacity. -- [#1276](https://github.com/keymapperorg/KeyMapper/issues/1276) Use key event scan code as fallback if the key code is unrecognized. +- [#1586](https://github.com/keymapperorg/KeyMapper/issues/1586) 🎨 Customise floating button border + and background opacity. +- [#1276](https://github.com/keymapperorg/KeyMapper/issues/1276) Use key event scan code as fallback + if the key code is unrecognized. - Make it clearer that the instructions need to be read for the assistant trigger. ## Changed @@ -473,8 +592,10 @@ This is not an April Fool's joke ;) the status bar. - Do not show floating buttons on the always-on display or when the display is "off". - Prompt to unlock device when tapping "Go back" on the floating menu. -- [#1596](https://github.com/keymapperorg/KeyMapper/issues/1596) Do not show the option for front flashlight if the device does not have one. -- [#1598](https://github.com/keymapperorg/KeyMapper/issues/1598) Do not allow changing flashlight brightness on devices that do not support it. +- [#1596](https://github.com/keymapperorg/KeyMapper/issues/1596) Do not show the option for front + flashlight if the device does not have one. +- [#1598](https://github.com/keymapperorg/KeyMapper/issues/1598) Do not allow changing flashlight + brightness on devices that do not support it. - Omit "Back" from Back flashlight actions and constraints since most devices only have a back flashlight anyway. - Do not ask for which flashlight to use in constraints if the device only has one @@ -485,21 +606,25 @@ This is not an April Fool's joke ;) ## Added -- [#1560](https://github.com/keymapperorg/KeyMapper/issues/1560) Action to change flashlight brightness and also set a custom brightness when enabling the +- [#1560](https://github.com/keymapperorg/KeyMapper/issues/1560) Action to change flashlight + brightness and also set a custom brightness when enabling the flashlight. - Prompt to unlock device when using a floating button as a trigger from the lock screen ## Changed -- [#1577](https://github.com/keymapperorg/KeyMapper/issues/1577) Move unsupported actions to the bottom of the list and do not allow selecting root actions +- [#1577](https://github.com/keymapperorg/KeyMapper/issues/1577) Move unsupported actions to the + bottom of the list and do not allow selecting root actions if root permission is not granted. -- [#1593](https://github.com/keymapperorg/KeyMapper/issues/1593) Deprecate the 'Open menu' action by not letting new key maps use it. It is a relic of the +- [#1593](https://github.com/keymapperorg/KeyMapper/issues/1593) Deprecate the 'Open menu' action by + not letting new key maps use it. It is a relic of the past when most apps had a 3-dot menu with a consistent content description making it somewhat easy to identify. ## Bug fixes -- [#1585](https://github.com/keymapperorg/KeyMapper/issues/1585) Track changes when editing key maps and only prompt to discard changes if there were indeed +- [#1585](https://github.com/keymapperorg/KeyMapper/issues/1585) Track changes when editing key maps + and only prompt to discard changes if there were indeed changes. ## [3.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.1) @@ -511,26 +636,38 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Added -- [#1407](https://github.com/keymapperorg/KeyMapper/issues/1407) New trigger! Add floating buttons on top of other apps to input key maps. +- [#1407](https://github.com/keymapperorg/KeyMapper/issues/1407) New trigger! Add floating buttons + on top of other apps to input key maps. - Key maps are much more dense on the home screen. - Button to pause/resume key maps at the top of the home screen. -- [#1502](https://github.com/keymapperorg/KeyMapper/issues/1502) Constraint for lockscreen is (not) showing. -- [#1203](https://github.com/keymapperorg/KeyMapper/issues/1203) Show a share sheet after exporting key maps rather than asking where to store it. This +- [#1502](https://github.com/keymapperorg/KeyMapper/issues/1502) Constraint for lockscreen is (not) + showing. +- [#1203](https://github.com/keymapperorg/KeyMapper/issues/1203) Show a share sheet after exporting + key maps rather than asking where to store it. This solves the problem when no apps are installed to select where to back it up. You can still find the file in the Downloads file. -- [#1531](https://github.com/keymapperorg/KeyMapper/issues/1531) Show shortcuts to quickly add recently used actions and constraints. -- [#1487](https://github.com/keymapperorg/KeyMapper/issues/1487) Add confirmation dialog when importing key maps and offer the option to replace all the key +- [#1531](https://github.com/keymapperorg/KeyMapper/issues/1531) Show shortcuts to quickly add + recently used actions and constraints. +- [#1487](https://github.com/keymapperorg/KeyMapper/issues/1487) Add confirmation dialog when + importing key maps and offer the option to replace all the key maps or append to the list. -- [#1546](https://github.com/keymapperorg/KeyMapper/issues/1546) Add short explanation of what constraints mean on top of the list. -- [#1548](https://github.com/keymapperorg/KeyMapper/issues/1548) Dynamically change key map enabled switch label. -- [#1562](https://github.com/keymapperorg/KeyMapper/issues/1562) Import key maps by opening .json and .zip files from other apps and file managers. +- [#1546](https://github.com/keymapperorg/KeyMapper/issues/1546) Add short explanation of what + constraints mean on top of the list. +- [#1548](https://github.com/keymapperorg/KeyMapper/issues/1548) Dynamically change key map enabled + switch label. +- [#1562](https://github.com/keymapperorg/KeyMapper/issues/1562) Import key maps by opening .json + and .zip files from other apps and file managers. ## Bug fixes -- [#1518](https://github.com/keymapperorg/KeyMapper/issues/1518) detect more apps that are playing media (fix to previous fix). -- [#1545](https://github.com/keymapperorg/KeyMapper/issues/1545) support phone call constraints in more apps. -- [#1536](https://github.com/keymapperorg/KeyMapper/issues/1536) 'Edit action' sometimes does not appear. -- [#1507](https://github.com/keymapperorg/KeyMapper/issues/1507) only vibrate once when mixing short, long, and double press key maps. +- [#1518](https://github.com/keymapperorg/KeyMapper/issues/1518) detect more apps that are playing + media (fix to previous fix). +- [#1545](https://github.com/keymapperorg/KeyMapper/issues/1545) support phone call constraints in + more apps. +- [#1536](https://github.com/keymapperorg/KeyMapper/issues/1536) 'Edit action' sometimes does not + appear. +- [#1507](https://github.com/keymapperorg/KeyMapper/issues/1507) only vibrate once when mixing + short, long, and double press key maps. - Prevent various system errors from crashing the apps. ## [2.8.3](https://github.com/sds100/KeyMapper/releases/tag/v2.8.3) @@ -539,12 +676,15 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Changed -- [#1474](https://github.com/keymapperorg/KeyMapper/issues/1474) always allow specifying a name for key map launcher shortcuts. -- [#1533](https://github.com/keymapperorg/KeyMapper/issues/1533) simplify naming of ringer mode actions. +- [#1474](https://github.com/keymapperorg/KeyMapper/issues/1474) always allow specifying a name for + key map launcher shortcuts. +- [#1533](https://github.com/keymapperorg/KeyMapper/issues/1533) simplify naming of ringer mode + actions. ## Bug fixes -- [#1535](https://github.com/keymapperorg/KeyMapper/issues/1535) side key/assistant trigger does not trigger from non-assistant buttons. +- [#1535](https://github.com/keymapperorg/KeyMapper/issues/1535) side key/assistant trigger does not + trigger from non-assistant buttons. ## [2.8.2](https://github.com/sds100/KeyMapper/releases/tag/v2.8.2) @@ -552,13 +692,16 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Changes -- [#1514](https://github.com/keymapperorg/KeyMapper/issues/1514), [#1454](https://github.com/keymapperorg/KeyMapper/issues/1454) Improving naming of assistant trigger to also refer to the side key and do not force +- [#1514](https://github.com/keymapperorg/KeyMapper/issues/1514), [#1454](https://github.com/keymapperorg/KeyMapper/issues/1454) + Improving naming of assistant trigger to also refer to the side key and do not force the user to select Key Mapper as the assistant. ## Bug fixes -- [#1461](https://github.com/keymapperorg/KeyMapper/issues/1461) fix: crash on startup due to getting MotionEvent device -- [#1518](https://github.com/keymapperorg/KeyMapper/issues/1518) fix: detect apps playing media without a notification for media constraints +- [#1461](https://github.com/keymapperorg/KeyMapper/issues/1461) fix: crash on startup due to + getting MotionEvent device +- [#1518](https://github.com/keymapperorg/KeyMapper/issues/1518) fix: detect apps playing media + without a notification for media constraints ## [2.8.1](https://github.com/sds100/KeyMapper/releases/tag/v2.8.1) @@ -566,14 +709,21 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Bug fixes -- [#1433](https://github.com/keymapperorg/KeyMapper/issues/1433) open Key Mapper by default and not the Assistant Trigger app. -- [#1386](https://github.com/keymapperorg/KeyMapper/issues/1386) wait for sequence trigger timeout before triggering other overlapping triggers. -- [#1449](https://github.com/keymapperorg/KeyMapper/issues/1449) improve the key mapper crashed dialog. -- [#1415](https://github.com/keymapperorg/KeyMapper/issues/1415) make the discard changes dialog less confusing. -- [#1440](https://github.com/keymapperorg/KeyMapper/issues/1440) do not show the "Button not detected?" bottom sheet every time you open the config key map +- [#1433](https://github.com/keymapperorg/KeyMapper/issues/1433) open Key Mapper by default and not + the Assistant Trigger app. +- [#1386](https://github.com/keymapperorg/KeyMapper/issues/1386) wait for sequence trigger timeout + before triggering other overlapping triggers. +- [#1449](https://github.com/keymapperorg/KeyMapper/issues/1449) improve the key mapper crashed + dialog. +- [#1415](https://github.com/keymapperorg/KeyMapper/issues/1415) make the discard changes dialog + less confusing. +- [#1440](https://github.com/keymapperorg/KeyMapper/issues/1440) do not show the "Button not + detected?" bottom sheet every time you open the config key map screen in some cases. -- [#1447](https://github.com/keymapperorg/KeyMapper/issues/1447) the app bar when configuring an Intent action would extend to the top of the screen. -- [#1444](https://github.com/keymapperorg/KeyMapper/issues/1444) use the correct icon for screen on/off constraints. +- [#1447](https://github.com/keymapperorg/KeyMapper/issues/1447) the app bar when configuring an + Intent action would extend to the top of the screen. +- [#1444](https://github.com/keymapperorg/KeyMapper/issues/1444) use the correct icon for screen + on/off constraints. ## [2.8.0](https://github.com/sds100/KeyMapper/releases/tag/v2.8.0) @@ -582,29 +732,42 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Added - [#491](https://github.com/keymapperorg/KeyMapper/issues/491) remap DPAD buttons. -- [#1223](https://github.com/keymapperorg/KeyMapper/issues/1223) sort key maps by triggers, actions, constraints and options. -- [#1344](https://github.com/keymapperorg/KeyMapper/issues/1344) target Android 15 and support edge-to-edge display mode. -- [#1372](https://github.com/keymapperorg/KeyMapper/issues/1372) allow Shizuku features to work with Sui. -- [#1391](https://github.com/keymapperorg/KeyMapper/issues/1391) button in Settings to reset all settings to their defaults. +- [#1223](https://github.com/keymapperorg/KeyMapper/issues/1223) sort key maps by triggers, actions, + constraints and options. +- [#1344](https://github.com/keymapperorg/KeyMapper/issues/1344) target Android 15 and support + edge-to-edge display mode. +- [#1372](https://github.com/keymapperorg/KeyMapper/issues/1372) allow Shizuku features to work with + Sui. +- [#1391](https://github.com/keymapperorg/KeyMapper/issues/1391) button in Settings to reset all + settings to their defaults. ## Changed -- [#1412](https://github.com/keymapperorg/KeyMapper/issues/1412) make the record trigger text clearer by saying it is recording. +- [#1412](https://github.com/keymapperorg/KeyMapper/issues/1412) make the record trigger text + clearer by saying it is recording. ## Removed -- [#1411](https://github.com/keymapperorg/KeyMapper/issues/1411) remove the app intro screen for remapping fingerprint gestures because almost all new phones +- [#1411](https://github.com/keymapperorg/KeyMapper/issues/1411) remove the app intro screen for + remapping fingerprint gestures because almost all new phones do not support them anyway. ## Bug fixes -- [#1426](https://github.com/keymapperorg/KeyMapper/issues/1426), [#1434](https://github.com/keymapperorg/KeyMapper/issues/1434) key map launcher shortcut icons were white. -- [#1410](https://github.com/keymapperorg/KeyMapper/issues/1410) vibrations not working on Android 13+. -- [#1342](https://github.com/keymapperorg/KeyMapper/issues/1342) add missing Meta modifier options for key event actions. -- [#1375](https://github.com/keymapperorg/KeyMapper/issues/1375) memory leak when rebinding to the relay service in the Key Mapper GUI Keyboard. -- [#1376](https://github.com/keymapperorg/KeyMapper/issues/1376) Key Mapper Basic Input Method would not work on Android 14+ in some situations. -- [#1094](https://github.com/keymapperorg/KeyMapper/issues/1094) wrong repository name in the introduction screen. -- [#1387](https://github.com/keymapperorg/KeyMapper/issues/1387) some app shortcuts would not open on Android 14+. +- [#1426](https://github.com/keymapperorg/KeyMapper/issues/1426), [#1434](https://github.com/keymapperorg/KeyMapper/issues/1434) + key map launcher shortcut icons were white. +- [#1410](https://github.com/keymapperorg/KeyMapper/issues/1410) vibrations not working on Android + 13+. +- [#1342](https://github.com/keymapperorg/KeyMapper/issues/1342) add missing Meta modifier options + for key event actions. +- [#1375](https://github.com/keymapperorg/KeyMapper/issues/1375) memory leak when rebinding to the + relay service in the Key Mapper GUI Keyboard. +- [#1376](https://github.com/keymapperorg/KeyMapper/issues/1376) Key Mapper Basic Input Method would + not work on Android 14+ in some situations. +- [#1094](https://github.com/keymapperorg/KeyMapper/issues/1094) wrong repository name in the + introduction screen. +- [#1387](https://github.com/keymapperorg/KeyMapper/issues/1387) some app shortcuts would not open + on Android 14+. ## [2.7.2](https://github.com/sds100/KeyMapper/releases/tag/v2.7.2) @@ -612,15 +775,20 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Added -- [#1298](https://github.com/keymapperorg/KeyMapper/issues/1298) add action to launch the Android device controls screen for managing Home devices. +- [#1298](https://github.com/keymapperorg/KeyMapper/issues/1298) add action to launch the Android + device controls screen for managing Home devices. ## Bug fixes -- [#1342](https://github.com/keymapperorg/KeyMapper/issues/1342) add Meta modifier keys to the key event action. -- [#1101](https://github.com/keymapperorg/KeyMapper/issues/1101) deprecate the toggle split screen action on Android 12L and newer. -- [#1370](https://github.com/keymapperorg/KeyMapper/issues/1370) warn the user that extra permissions are required for the Launch app action on Xiaomi +- [#1342](https://github.com/keymapperorg/KeyMapper/issues/1342) add Meta modifier keys to the key + event action. +- [#1101](https://github.com/keymapperorg/KeyMapper/issues/1101) deprecate the toggle split screen + action on Android 12L and newer. +- [#1370](https://github.com/keymapperorg/KeyMapper/issues/1370) warn the user that extra + permissions are required for the Launch app action on Xiaomi devices -- [#1371](https://github.com/keymapperorg/KeyMapper/issues/1371) try to fix the app not opening on people's devices +- [#1371](https://github.com/keymapperorg/KeyMapper/issues/1371) try to fix the app not opening on + people's devices ## [2.7.1](https://github.com/sds100/KeyMapper/releases/tag/v2.7.1) @@ -628,9 +796,12 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Bug fixes -- [#1360](https://github.com/keymapperorg/KeyMapper/issues/1360) complete the documentation for advanced triggers at docs.keymapper.club. -- [#1364](https://github.com/keymapperorg/KeyMapper/issues/1364) key event actions no longer crash when using Shizuku. -- [#1362](https://github.com/keymapperorg/KeyMapper/issues/1362) backing up and restoring key maps works again. +- [#1360](https://github.com/keymapperorg/KeyMapper/issues/1360) complete the documentation for + advanced triggers at docs.keymapper.club. +- [#1364](https://github.com/keymapperorg/KeyMapper/issues/1364) key event actions no longer crash + when using Shizuku. +- [#1362](https://github.com/keymapperorg/KeyMapper/issues/1362) backing up and restoring key maps + works again. ## [2.7.0](https://github.com/sds100/KeyMapper/releases/tag/v2.7.0) @@ -638,21 +809,25 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Added -- [#1274](https://github.com/keymapperorg/KeyMapper/issues/1274) New trigger! You can now trigger your key maps from any of the ways your phone launches the +- [#1274](https://github.com/keymapperorg/KeyMapper/issues/1274) New trigger! You can now trigger + your key maps from any of the ways your phone launches the assistant! This could be the Bixby button, Power button, or a button on your headset. - [#1304](https://github.com/keymapperorg/KeyMapper/issues/1304) Vietnamese translations. ## Bug fixes -- [#1222](https://github.com/keymapperorg/KeyMapper/issues/1222) [#1307](https://github.com/keymapperorg/KeyMapper/issues/1307) Key Mapper doesn't execute the correct app shortcut action if you created multiple +- [#1222](https://github.com/keymapperorg/KeyMapper/issues/1222) [#1307](https://github.com/keymapperorg/KeyMapper/issues/1307) + Key Mapper doesn't execute the correct app shortcut action if you created multiple from the same app. -- [#1328](https://github.com/keymapperorg/KeyMapper/issues/1328) Single-character non-ASCII TEXT_BLOCK input crashes the service +- [#1328](https://github.com/keymapperorg/KeyMapper/issues/1328) Single-character non-ASCII + TEXT_BLOCK input crashes the service ## [2.6.2](https://github.com/sds100/KeyMapper/releases/tag/v2.6.2) #### 9 September 2024 -- [#1293](https://github.com/keymapperorg/KeyMapper/issues/1293) Checkbox buttons were invisible when configuring some actions. +- [#1293](https://github.com/keymapperorg/KeyMapper/issues/1293) Checkbox buttons were invisible + when configuring some actions. ## [2.6.1](https://github.com/sds100/KeyMapper/releases/tag/v2.6.1) @@ -662,29 +837,42 @@ This release adds support for Android 14 and fixes some bugs associated with it. ### Added -- [#1256](https://github.com/keymapperorg/KeyMapper/issues/1256) Add Russian and Chinese Simplified translations. Update other languages. -- [#1282](https://github.com/keymapperorg/KeyMapper/issues/1282) Add Assist key code as screen off trigger for Bixby button. +- [#1256](https://github.com/keymapperorg/KeyMapper/issues/1256) Add Russian and Chinese Simplified + translations. Update other languages. +- [#1282](https://github.com/keymapperorg/KeyMapper/issues/1282) Add Assist key code as screen off + trigger for Bixby button. ### Bug fixes -- [#1218](https://github.com/keymapperorg/KeyMapper/issues/1218), [#1251](https://github.com/keymapperorg/KeyMapper/issues/1251) Key event actions and triggering key maps from an intent were delayed by 1 second on +- [#1218](https://github.com/keymapperorg/KeyMapper/issues/1218), [#1251](https://github.com/keymapperorg/KeyMapper/issues/1251) + Key event actions and triggering key maps from an intent were delayed by 1 second on Android 14 due to new broadcast receiver restrictions. -- [#1175](https://github.com/keymapperorg/KeyMapper/issues/1175) Bypass the do not disturb permission requirement for volume button triggers. -- [#1234](https://github.com/keymapperorg/KeyMapper/issues/1234) Granting permissions with Shizuku crashes on Android 14. -- [#1249](https://github.com/keymapperorg/KeyMapper/issues/1249) Crash when opening help page from the home page if no browser app for custom tabs was found. -- [#1250](https://github.com/keymapperorg/KeyMapper/issues/1250) Random crashes when picking a screenshot for actions. -- [#1227](https://github.com/keymapperorg/KeyMapper/issues/1227) Deprecate Bluetooth actions on Android 13+ due to new restrictions. -- [#1252](https://github.com/keymapperorg/KeyMapper/issues/1252) Add another Camera key code as supported for screen off triggers. -- [#1219](https://github.com/keymapperorg/KeyMapper/issues/1219) Key Mapper notifications could not be enabled on Android 14. -- [#1194](https://github.com/keymapperorg/KeyMapper/issues/1194) Deprecate closing the status bar on Android 14 due to new restrictions. -- [#1190](https://github.com/keymapperorg/KeyMapper/issues/1190) Add a 3 second delay after the screenshot action before showing the on-screen message +- [#1175](https://github.com/keymapperorg/KeyMapper/issues/1175) Bypass the do not disturb + permission requirement for volume button triggers. +- [#1234](https://github.com/keymapperorg/KeyMapper/issues/1234) Granting permissions with Shizuku + crashes on Android 14. +- [#1249](https://github.com/keymapperorg/KeyMapper/issues/1249) Crash when opening help page from + the home page if no browser app for custom tabs was found. +- [#1250](https://github.com/keymapperorg/KeyMapper/issues/1250) Random crashes when picking a + screenshot for actions. +- [#1227](https://github.com/keymapperorg/KeyMapper/issues/1227) Deprecate Bluetooth actions on + Android 13+ due to new restrictions. +- [#1252](https://github.com/keymapperorg/KeyMapper/issues/1252) Add another Camera key code as + supported for screen off triggers. +- [#1219](https://github.com/keymapperorg/KeyMapper/issues/1219) Key Mapper notifications could not + be enabled on Android 14. +- [#1194](https://github.com/keymapperorg/KeyMapper/issues/1194) Deprecate closing the status bar on + Android 14 due to new restrictions. +- [#1190](https://github.com/keymapperorg/KeyMapper/issues/1190) Add a 3 second delay after the + screenshot action before showing the on-screen message confirming it happened. ## [2.6.0](https://github.com/sds100/KeyMapper/releases/tag/v2.6.0) #### 7 October 2023 -- [#550](https://github.com/keymapperorg/KeyMapper/issues/550) Action for doing pinches and swipes on the screen with 2 or more fingers. Many thanks to +- [#550](https://github.com/keymapperorg/KeyMapper/issues/550) Action for doing pinches and swipes + on the screen with 2 or more fingers. Many thanks to Tino (@pixel-shock) for working on this feature. 😊 ## [2.5.0](https://github.com/sds100/KeyMapper/releases/tag/v2.5.0) @@ -1057,61 +1245,91 @@ These are all the changes from 2.2.0. - 🎉 A new website with a tutorial! 🎉 [docs.keymapper.club](https://docs.keymapper.club) -- Action to broadcast intent, start activity and start service. [#112](https://github.com/keymapperorg/KeyMapper/issues/112) -- Action to show the input method picker by using the Key Mapper keyboard. [#531](https://github.com/keymapperorg/KeyMapper/issues/531) -- Action to toggle the notification drawer and the quick settings drawer. [#242](https://github.com/keymapperorg/KeyMapper/issues/242) +- Action to broadcast intent, start activity and start + service. [#112](https://github.com/keymapperorg/KeyMapper/issues/112) +- Action to show the input method picker by using the Key Mapper + keyboard. [#531](https://github.com/keymapperorg/KeyMapper/issues/531) +- Action to toggle the notification drawer and the quick settings + drawer. [#242](https://github.com/keymapperorg/KeyMapper/issues/242) - Action to call a phone number. [#516](https://github.com/keymapperorg/KeyMapper/issues/516) - Action to play a sound. - A workaround for the Android 11 bug that sets the language of external keyboards to English-US - when an accessibility service is enabled. [#618](https://github.com/keymapperorg/KeyMapper/issues/618) Read the guide + when an accessibility service is + enabled. [#618](https://github.com/keymapperorg/KeyMapper/issues/618) Read the guide here https://docs.keymapper.club/redirects/android-11-device-id-bug-work-around - Prompt the user to read the quick start guide on the website the first time the app is opened. [#544](https://github.com/keymapperorg/KeyMapper/issues/544) -- Links to a relevant online guide in each screen in the app. [#539](https://github.com/keymapperorg/KeyMapper/issues/539) -- Option in key event action to input the key event through the shell. [#559](https://github.com/keymapperorg/KeyMapper/issues/559) +- Links to a relevant online guide in each screen in the + app. [#539](https://github.com/keymapperorg/KeyMapper/issues/539) +- Option in key event action to input the key event through the + shell. [#559](https://github.com/keymapperorg/KeyMapper/issues/559) - Splash screen [#561](https://github.com/keymapperorg/KeyMapper/issues/561) -- Data migrations when restoring from backups. [#574](https://github.com/keymapperorg/KeyMapper/issues/574) -- Enable hold down and disable repeat by default for modifier key actions. [#579](https://github.com/keymapperorg/KeyMapper/issues/579) -- Ability to change the input method with the accessibility service on Android 11+. [#619](https://github.com/keymapperorg/KeyMapper/issues/619) -- Make it clearer that selecting a screenshot to set up a tap coordinate action is optional. [#632](https://github.com/keymapperorg/KeyMapper/issues/632) -- Show a prompt to install the Key Mapper GUI Keyboard when a key event action is created. [#645](https://github.com/keymapperorg/KeyMapper/issues/645) -- Back up default key map settings in back ups. [#659](https://github.com/keymapperorg/KeyMapper/issues/659) -- Warnings when the accessibility service is turned on but isn't actually running. [#643](https://github.com/keymapperorg/KeyMapper/issues/643) -- Show a message at the top of the home screen when mappings are paused. [#642](https://github.com/keymapperorg/KeyMapper/issues/642) -- A caution message to avoid locking the user when using screen pinning mode. [#602](https://github.com/keymapperorg/KeyMapper/issues/602) -- A logging page in the app which can be used instead of bug reports. [#651](https://github.com/keymapperorg/KeyMapper/issues/651) -- A button in the settings to reset sliders to their default. [#589](https://github.com/keymapperorg/KeyMapper/issues/589) +- Data migrations when restoring from + backups. [#574](https://github.com/keymapperorg/KeyMapper/issues/574) +- Enable hold down and disable repeat by default for modifier key + actions. [#579](https://github.com/keymapperorg/KeyMapper/issues/579) +- Ability to change the input method with the accessibility service on Android + 11+. [#619](https://github.com/keymapperorg/KeyMapper/issues/619) +- Make it clearer that selecting a screenshot to set up a tap coordinate action is + optional. [#632](https://github.com/keymapperorg/KeyMapper/issues/632) +- Show a prompt to install the Key Mapper GUI Keyboard when a key event action is + created. [#645](https://github.com/keymapperorg/KeyMapper/issues/645) +- Back up default key map settings in back + ups. [#659](https://github.com/keymapperorg/KeyMapper/issues/659) +- Warnings when the accessibility service is turned on but isn't actually + running. [#643](https://github.com/keymapperorg/KeyMapper/issues/643) +- Show a message at the top of the home screen when mappings are + paused. [#642](https://github.com/keymapperorg/KeyMapper/issues/642) +- A caution message to avoid locking the user when using screen pinning + mode. [#602](https://github.com/keymapperorg/KeyMapper/issues/602) +- A logging page in the app which can be used instead of bug + reports. [#651](https://github.com/keymapperorg/KeyMapper/issues/651) +- A button in the settings to reset sliders to their + default. [#589](https://github.com/keymapperorg/KeyMapper/issues/589) - A repeat limit action option. [#663](https://github.com/keymapperorg/KeyMapper/issues/663) - Show a dialog before resetting fingerprint gesture maps. -- A new Key Mapper keyboard that is designed for Android TV. [#493](https://github.com/keymapperorg/KeyMapper/issues/493) -- An Intent API to pause/resume key maps. [#668](https://github.com/keymapperorg/KeyMapper/issues/668) -- Allow Key Mapper to be launched from the Android TV launcher. [#695](https://github.com/keymapperorg/KeyMapper/issues/695) -- Make it much easier to report bugs and turn off aggressive app killing. [#728](https://github.com/keymapperorg/KeyMapper/issues/728) There is now a button +- A new Key Mapper keyboard that is designed for Android + TV. [#493](https://github.com/keymapperorg/KeyMapper/issues/493) +- An Intent API to pause/resume key + maps. [#668](https://github.com/keymapperorg/KeyMapper/issues/668) +- Allow Key Mapper to be launched from the Android TV + launcher. [#695](https://github.com/keymapperorg/KeyMapper/issues/695) +- Make it much easier to report bugs and turn off aggressive app + killing. [#728](https://github.com/keymapperorg/KeyMapper/issues/728) There is now a button in the home screen menu to send a bug report and the user is now prompted to read dontkillmyapp.com when the accessibility service crashes. -- Support for repeat until limit reached action option in fingerprint gesture maps. [#710](https://github.com/keymapperorg/KeyMapper/issues/710) +- Support for repeat until limit reached action option in fingerprint gesture + maps. [#710](https://github.com/keymapperorg/KeyMapper/issues/710) - Polish translations. - Czech translations. ### Changed -- Move action option to show a toast message to the same place as the vibrate option. [#565](https://github.com/keymapperorg/KeyMapper/issues/565) +- Move action option to show a toast message to the same place as the vibrate + option. [#565](https://github.com/keymapperorg/KeyMapper/issues/565) - Replace setting to choose Bluetooth device in settings with setting to choose any input device. [#620](https://github.com/keymapperorg/KeyMapper/issues/620) -- Rename 'action count' option to 'how many times'. [#611](https://github.com/keymapperorg/KeyMapper/issues/611) -- Move option to show the volume ui for an action to when the action is created. [#639](https://github.com/keymapperorg/KeyMapper/issues/639) -- Tapping the pause/resume key maps notification now opens Key Mapper. [#665](https://github.com/keymapperorg/KeyMapper/issues/665) -- Make action descriptions more descriptive when repeat is turned on. [#666](https://github.com/keymapperorg/KeyMapper/issues/666) +- Rename 'action count' option to 'how many + times'. [#611](https://github.com/keymapperorg/KeyMapper/issues/611) +- Move option to show the volume ui for an action to when the action is + created. [#639](https://github.com/keymapperorg/KeyMapper/issues/639) +- Tapping the pause/resume key maps notification now opens Key + Mapper. [#665](https://github.com/keymapperorg/KeyMapper/issues/665) +- Make action descriptions more descriptive when repeat is turned + on. [#666](https://github.com/keymapperorg/KeyMapper/issues/666) - Alerts at the top of the home screen have been simplified. ### Removed -- Dex slide in the app intro because it didn't work. [#646](https://github.com/keymapperorg/KeyMapper/issues/646) -- Buttons to enable all and disable all key maps in the home screen menu. [#647](https://github.com/keymapperorg/KeyMapper/issues/647) -- Support for Android KitKat 4.4 and older. [#627](https://github.com/keymapperorg/KeyMapper/issues/627) +- Dex slide in the app intro because it didn't + work. [#646](https://github.com/keymapperorg/KeyMapper/issues/646) +- Buttons to enable all and disable all key maps in the home screen + menu. [#647](https://github.com/keymapperorg/KeyMapper/issues/647) +- Support for Android KitKat 4.4 and + older. [#627](https://github.com/keymapperorg/KeyMapper/issues/627) - Ability to view changelog, license and privacy policy in an in-app dialog. They now open a link in the browser. [#648](https://github.com/keymapperorg/KeyMapper/issues/648) - Alerts at the top of the home screen to enable a Key Mapper keyboard, grant WRITE_SECURE_SETTINGS @@ -1128,7 +1346,8 @@ See the 2.3.0 Beta releases below. ### Changes - Never show the "key mapper has crashed" dialog automatically since this causes a lot of confusion. -- Prompt the user to restart the accessibility service rather than report a bug. [#736](https://github.com/keymapperorg/KeyMapper/issues/736) +- Prompt the user to restart the accessibility service rather than report a + bug. [#736](https://github.com/keymapperorg/KeyMapper/issues/736) ### Added @@ -1152,8 +1371,10 @@ See the 2.3.0 Beta releases below. ### Bug Fixes -- Write Secure Settings section in settings is enabled even if permission is revoked. [#732](https://github.com/keymapperorg/KeyMapper/issues/732) -- Many random crashes. [#744](https://github.com/keymapperorg/KeyMapper/issues/744), [#743](https://github.com/keymapperorg/KeyMapper/issues/743), [#742](https://github.com/keymapperorg/KeyMapper/issues/742), [#741](https://github.com/keymapperorg/KeyMapper/issues/741), [#740](https://github.com/keymapperorg/KeyMapper/issues/740), [#738](https://github.com/keymapperorg/KeyMapper/issues/738), [#737](https://github.com/keymapperorg/KeyMapper/issues/737) +- Write Secure Settings section in settings is enabled even if permission is + revoked. [#732](https://github.com/keymapperorg/KeyMapper/issues/732) +- Many random + crashes. [#744](https://github.com/keymapperorg/KeyMapper/issues/744), [#743](https://github.com/keymapperorg/KeyMapper/issues/743), [#742](https://github.com/keymapperorg/KeyMapper/issues/742), [#741](https://github.com/keymapperorg/KeyMapper/issues/741), [#740](https://github.com/keymapperorg/KeyMapper/issues/740), [#738](https://github.com/keymapperorg/KeyMapper/issues/738), [#737](https://github.com/keymapperorg/KeyMapper/issues/737) - Don't crash when restoring back ups without a sounds folder in it. - Don't restore a back up from a newer version of key mapper to prevent the app crashing when reading the restored data. @@ -1164,19 +1385,25 @@ See the 2.3.0 Beta releases below. ### Added -- Make it much easier to report bugs and turn off aggressive app killing. [#728](https://github.com/keymapperorg/KeyMapper/issues/728) There is now a button +- Make it much easier to report bugs and turn off aggressive app + killing. [#728](https://github.com/keymapperorg/KeyMapper/issues/728) There is now a button in the home screen menu to send a bug report and the user is now prompted to read dontkillmyapp.com when the accessibility service crashes. - Action to play a sound ### Bug Fixes -- Close notification drawer after the notification has been pressed. [#719](https://github.com/keymapperorg/KeyMapper/issues/719) +- Close notification drawer after the notification has been + pressed. [#719](https://github.com/keymapperorg/KeyMapper/issues/719) - Crash if couldn't find input device. [#730](https://github.com/keymapperorg/KeyMapper/issues/730) -- Crash if couldn't find chosen input method. [#731](https://github.com/keymapperorg/KeyMapper/issues/731) -- Crash when failing to get package info. [#721](https://github.com/keymapperorg/KeyMapper/issues/721) -- Crash if couldn't find Bluetooth device. [#723](https://github.com/keymapperorg/KeyMapper/issues/723) -- Crash when disabling accessibility service. [#720](https://github.com/keymapperorg/KeyMapper/issues/720) +- Crash if couldn't find chosen input + method. [#731](https://github.com/keymapperorg/KeyMapper/issues/731) +- Crash when failing to get package + info. [#721](https://github.com/keymapperorg/KeyMapper/issues/721) +- Crash if couldn't find Bluetooth + device. [#723](https://github.com/keymapperorg/KeyMapper/issues/723) +- Crash when disabling accessibility + service. [#720](https://github.com/keymapperorg/KeyMapper/issues/720) - Reduce memory usage. [#725](https://github.com/keymapperorg/KeyMapper/issues/725) - Ensure log doesn't grow forever. [#729](https://github.com/keymapperorg/KeyMapper/issues/729) @@ -1186,12 +1413,14 @@ See the 2.3.0 Beta releases below. ### Added -- Support for repeat until limit reached action option in fingerprint gesture maps. [#710](https://github.com/keymapperorg/KeyMapper/issues/710) +- Support for repeat until limit reached action option in fingerprint gesture + maps. [#710](https://github.com/keymapperorg/KeyMapper/issues/710) ### Bug Fixes - Crash on start up on some devices. [#706](https://github.com/keymapperorg/KeyMapper/issues/706) -- Notification advertising fingerprint gesture maps is shown on every update [#709](https://github.com/keymapperorg/KeyMapper/issues/709) +- Notification advertising fingerprint gesture maps is shown on every + update [#709](https://github.com/keymapperorg/KeyMapper/issues/709) - Key map launcher shortcut repeats indefinitely when triggered if repeat until released is chosen. [#707](https://github.com/keymapperorg/KeyMapper/issues/707) @@ -1206,52 +1435,80 @@ See the 2.3.0 Beta releases below. - 🎉 A new website with a tutorial! 🎉 [docs.keymapper.club](https://docs.keymapper.club) -- Action to broadcast intent, start activity and start service. [#112](https://github.com/keymapperorg/KeyMapper/issues/112) -- Action to show the input method picker by using the Key Mapper keyboard. [#531](https://github.com/keymapperorg/KeyMapper/issues/531) -- Action to toggle the notification drawer and the quick settings drawer. [#242](https://github.com/keymapperorg/KeyMapper/issues/242) +- Action to broadcast intent, start activity and start + service. [#112](https://github.com/keymapperorg/KeyMapper/issues/112) +- Action to show the input method picker by using the Key Mapper + keyboard. [#531](https://github.com/keymapperorg/KeyMapper/issues/531) +- Action to toggle the notification drawer and the quick settings + drawer. [#242](https://github.com/keymapperorg/KeyMapper/issues/242) - Action to call a phone number. [#516](https://github.com/keymapperorg/KeyMapper/issues/516) - A workaround for the Android 11 bug that sets the language of external keyboards to English-US - when an accessibility service is enabled. [#618](https://github.com/keymapperorg/KeyMapper/issues/618) Read the guide + when an accessibility service is + enabled. [#618](https://github.com/keymapperorg/KeyMapper/issues/618) Read the guide here https://docs.keymapper.club/redirects/android-11-device-id-bug-work-around - Prompt the user to read the quick start guide on the website the first time the app is opened. [#544](https://github.com/keymapperorg/KeyMapper/issues/544) -- Links to a relevant online guide in each screen in the app. [#539](https://github.com/keymapperorg/KeyMapper/issues/539) -- Option in key event action to input the key event through the shell. [#559](https://github.com/keymapperorg/KeyMapper/issues/559) +- Links to a relevant online guide in each screen in the + app. [#539](https://github.com/keymapperorg/KeyMapper/issues/539) +- Option in key event action to input the key event through the + shell. [#559](https://github.com/keymapperorg/KeyMapper/issues/559) - Splash screen [#561](https://github.com/keymapperorg/KeyMapper/issues/561) -- Data migrations when restoring from backups. [#574](https://github.com/keymapperorg/KeyMapper/issues/574) -- Enable hold down and disable repeat by default for modifier key actions. [#579](https://github.com/keymapperorg/KeyMapper/issues/579) -- Ability to change the input method with the accessibility service on Android 11+. [#619](https://github.com/keymapperorg/KeyMapper/issues/619) -- Make it clearer that selecting a screenshot to set up a tap coordinate action is optional. [#632](https://github.com/keymapperorg/KeyMapper/issues/632) -- Show a prompt to install the Key Mapper GUI Keyboard when a key event action is created. [#645](https://github.com/keymapperorg/KeyMapper/issues/645) -- Back up default key map settings in back ups. [#659](https://github.com/keymapperorg/KeyMapper/issues/659) -- Warnings when the accessibility service is turned on but isn't actually running. [#643](https://github.com/keymapperorg/KeyMapper/issues/643) -- Show a message at the top of the home screen when mappings are paused. [#642](https://github.com/keymapperorg/KeyMapper/issues/642) -- A caution message to avoid locking the user when using screen pinning mode. [#602](https://github.com/keymapperorg/KeyMapper/issues/602) -- A logging page in the app which can be used instead of bug reports. [#651](https://github.com/keymapperorg/KeyMapper/issues/651) -- A button in the settings to reset sliders to their default. [#589](https://github.com/keymapperorg/KeyMapper/issues/589) +- Data migrations when restoring from + backups. [#574](https://github.com/keymapperorg/KeyMapper/issues/574) +- Enable hold down and disable repeat by default for modifier key + actions. [#579](https://github.com/keymapperorg/KeyMapper/issues/579) +- Ability to change the input method with the accessibility service on Android + 11+. [#619](https://github.com/keymapperorg/KeyMapper/issues/619) +- Make it clearer that selecting a screenshot to set up a tap coordinate action is + optional. [#632](https://github.com/keymapperorg/KeyMapper/issues/632) +- Show a prompt to install the Key Mapper GUI Keyboard when a key event action is + created. [#645](https://github.com/keymapperorg/KeyMapper/issues/645) +- Back up default key map settings in back + ups. [#659](https://github.com/keymapperorg/KeyMapper/issues/659) +- Warnings when the accessibility service is turned on but isn't actually + running. [#643](https://github.com/keymapperorg/KeyMapper/issues/643) +- Show a message at the top of the home screen when mappings are + paused. [#642](https://github.com/keymapperorg/KeyMapper/issues/642) +- A caution message to avoid locking the user when using screen pinning + mode. [#602](https://github.com/keymapperorg/KeyMapper/issues/602) +- A logging page in the app which can be used instead of bug + reports. [#651](https://github.com/keymapperorg/KeyMapper/issues/651) +- A button in the settings to reset sliders to their + default. [#589](https://github.com/keymapperorg/KeyMapper/issues/589) - A repeat limit action option. [#663](https://github.com/keymapperorg/KeyMapper/issues/663) - Show a dialog before resetting fingerprint gesture maps. -- A new Key Mapper keyboard that is designed for Android TV. [#493](https://github.com/keymapperorg/KeyMapper/issues/493) -- An Intent API to pause/resume key maps. [#668](https://github.com/keymapperorg/KeyMapper/issues/668) -- Allow Key Mapper to be launched from the Android TV launcher. [#695](https://github.com/keymapperorg/KeyMapper/issues/695) +- A new Key Mapper keyboard that is designed for Android + TV. [#493](https://github.com/keymapperorg/KeyMapper/issues/493) +- An Intent API to pause/resume key + maps. [#668](https://github.com/keymapperorg/KeyMapper/issues/668) +- Allow Key Mapper to be launched from the Android TV + launcher. [#695](https://github.com/keymapperorg/KeyMapper/issues/695) ### Changed -- Move action option to show a toast message to the same place as the vibrate option. [#565](https://github.com/keymapperorg/KeyMapper/issues/565) +- Move action option to show a toast message to the same place as the vibrate + option. [#565](https://github.com/keymapperorg/KeyMapper/issues/565) - Replace setting to choose Bluetooth device in settings with setting to choose any input device. [#620](https://github.com/keymapperorg/KeyMapper/issues/620) -- Rename 'action count' option to 'how many times'. [#611](https://github.com/keymapperorg/KeyMapper/issues/611) -- Move option to show the volume ui for an action to when the action is created. [#639](https://github.com/keymapperorg/KeyMapper/issues/639) -- Tapping the pause/resume key maps notification now opens Key Mapper. [#665](https://github.com/keymapperorg/KeyMapper/issues/665) -- Make action descriptions more descriptive when repeat is turned on. [#666](https://github.com/keymapperorg/KeyMapper/issues/666) +- Rename 'action count' option to 'how many + times'. [#611](https://github.com/keymapperorg/KeyMapper/issues/611) +- Move option to show the volume ui for an action to when the action is + created. [#639](https://github.com/keymapperorg/KeyMapper/issues/639) +- Tapping the pause/resume key maps notification now opens Key + Mapper. [#665](https://github.com/keymapperorg/KeyMapper/issues/665) +- Make action descriptions more descriptive when repeat is turned + on. [#666](https://github.com/keymapperorg/KeyMapper/issues/666) - Alerts at the top of the home screen have been simplified. ### Removed -- Dex slide in the app intro because it didn't work. [#646](https://github.com/keymapperorg/KeyMapper/issues/646) -- Buttons to enable all and disable all key maps in the home screen menu. [#647](https://github.com/keymapperorg/KeyMapper/issues/647) -- Support for Android KitKat 4.4 and older. [#627](https://github.com/keymapperorg/KeyMapper/issues/627) +- Dex slide in the app intro because it didn't + work. [#646](https://github.com/keymapperorg/KeyMapper/issues/646) +- Buttons to enable all and disable all key maps in the home screen + menu. [#647](https://github.com/keymapperorg/KeyMapper/issues/647) +- Support for Android KitKat 4.4 and + older. [#627](https://github.com/keymapperorg/KeyMapper/issues/627) - Ability to view changelog, license and privacy policy in an in-app dialog. They now open a link in the browser. [#648](https://github.com/keymapperorg/KeyMapper/issues/648) - Alerts at the top of the home screen to enable a Key Mapper keyboard, grant WRITE_SECURE_SETTINGS @@ -1263,20 +1520,30 @@ See the 2.3.0 Beta releases below. - Fix text consistency [#543](https://github.com/keymapperorg/KeyMapper/issues/543) - A parallel trigger which contains another parallel trigger after the first key should cancel the other. [#571](https://github.com/keymapperorg/KeyMapper/issues/571) -- Actions go off screen for key maps on the home screen. [#613](https://github.com/keymapperorg/KeyMapper/issues/613) -- Remove uses of Android framework strings for dialog buttons. [#650](https://github.com/keymapperorg/KeyMapper/issues/650) -- Trigger key click type sometimes resets to short press. [#615](https://github.com/keymapperorg/KeyMapper/issues/615) +- Actions go off screen for key maps on the home + screen. [#613](https://github.com/keymapperorg/KeyMapper/issues/613) +- Remove uses of Android framework strings for dialog + buttons. [#650](https://github.com/keymapperorg/KeyMapper/issues/650) +- Trigger key click type sometimes resets to short + press. [#615](https://github.com/keymapperorg/KeyMapper/issues/615) - Wrong device id is used when performing key event actions and there are multiple devices with the same descriptor. [#637](https://github.com/keymapperorg/KeyMapper/issues/637) -- Trigger key isn't imitated after a failed double press. [#606](https://github.com/keymapperorg/KeyMapper/issues/606) -- Actions don't start repeating on a failed long press or failed double press. [#626](https://github.com/keymapperorg/KeyMapper/issues/626) -- Crash when modifying a huge number of key maps. [#641](https://github.com/keymapperorg/KeyMapper/issues/641) -- Home menu is chopped off on screens with small height. [#582](https://github.com/keymapperorg/KeyMapper/issues/582) -- Crash when double pressing button to open action or trigger key options. [#600](https://github.com/keymapperorg/KeyMapper/issues/600) -- Some action options disappear when adding a new trigger key. [#594](https://github.com/keymapperorg/KeyMapper/issues/594) +- Trigger key isn't imitated after a failed double + press. [#606](https://github.com/keymapperorg/KeyMapper/issues/606) +- Actions don't start repeating on a failed long press or failed double + press. [#626](https://github.com/keymapperorg/KeyMapper/issues/626) +- Crash when modifying a huge number of key + maps. [#641](https://github.com/keymapperorg/KeyMapper/issues/641) +- Home menu is chopped off on screens with small + height. [#582](https://github.com/keymapperorg/KeyMapper/issues/582) +- Crash when double pressing button to open action or trigger key + options. [#600](https://github.com/keymapperorg/KeyMapper/issues/600) +- Some action options disappear when adding a new trigger + key. [#594](https://github.com/keymapperorg/KeyMapper/issues/594) - An action can continue to repeat even when the trigger is released if delay until next action is not 0. [#662](https://github.com/keymapperorg/KeyMapper/issues/662) -- A lot of input latency when using a lot of constraints. [#599](https://github.com/keymapperorg/KeyMapper/issues/599) +- A lot of input latency when using a lot of + constraints. [#599](https://github.com/keymapperorg/KeyMapper/issues/599) - Trigger button isn't imitated when a short press trigger with multiple keys fails to be triggered. [#664](https://github.com/keymapperorg/KeyMapper/issues/664) - Overlapping triggers. [#653](https://github.com/keymapperorg/KeyMapper/issues/653) @@ -1289,7 +1556,8 @@ This sums up all the changes for 2.2 ### Added -- Remap fingerprint gestures! [#378](https://github.com/keymapperorg/KeyMapper/issues/378) Android 8.0+ and only on devices which support them. Even devices +- Remap fingerprint gestures! [#378](https://github.com/keymapperorg/KeyMapper/issues/378) Android + 8.0+ and only on devices which support them. Even devices with the setting to swipe down for notifications might not support this! The dev can't do anything about this. @@ -1298,17 +1566,23 @@ This sums up all the changes for 2.2 differentiated in Key Mapper lists. [#470](https://github.com/keymapperorg/KeyMapper/issues/470) - Show a warning at the top of the homescreen if the user hasn't disabled battery optimisation for Key Mapper. [#496](https://github.com/keymapperorg/KeyMapper/issues/496) -- Action option to hold down until the trigger is pressed again. [#479](https://github.com/keymapperorg/KeyMapper/issues/479) -- Action option to change the delay before the next action in the list. [#476](https://github.com/keymapperorg/KeyMapper/issues/476) +- Action option to hold down until the trigger is pressed + again. [#479](https://github.com/keymapperorg/KeyMapper/issues/479) +- Action option to change the delay before the next action in the + list. [#476](https://github.com/keymapperorg/KeyMapper/issues/476) - Orientation constraint. [#505](https://github.com/keymapperorg/KeyMapper/issues/505) -- Key Event action option to pretend that the Key Event came from a particular device. [#509](https://github.com/keymapperorg/KeyMapper/issues/509) -- Use duplicates of the same key in a sequence trigger. [#513](https://github.com/keymapperorg/KeyMapper/issues/513) -- Show the fingerprint gesture intro slide when updating to 2.2 [#545](https://github.com/keymapperorg/KeyMapper/issues/545) +- Key Event action option to pretend that the Key Event came from a particular + device. [#509](https://github.com/keymapperorg/KeyMapper/issues/509) +- Use duplicates of the same key in a sequence + trigger. [#513](https://github.com/keymapperorg/KeyMapper/issues/513) +- Show the fingerprint gesture intro slide when updating to + 2.2 [#545](https://github.com/keymapperorg/KeyMapper/issues/545) - Show a silent notification, which advertises the remapping fingerprint gesture feature, when the user updates to 2.2 [#546](https://github.com/keymapperorg/KeyMapper/issues/546) - Trigger key maps from an Intent [#490](https://github.com/keymapperorg/KeyMapper/issues/490) - Prompt the user to go to https://dontkillmyapp.com when they first setup the app. -- Add Fdroid link to the Key Mapper GUI Keyboard ad. [#524](https://github.com/keymapperorg/KeyMapper/issues/524) +- Add Fdroid link to the Key Mapper GUI Keyboard + ad. [#524](https://github.com/keymapperorg/KeyMapper/issues/524) ### BREAKING CHANGES @@ -1317,7 +1591,8 @@ This sums up all the changes for 2.2 ### Changes -- No max limit for sliders (except in settings). [#458](https://github.com/keymapperorg/KeyMapper/issues/458) +- No max limit for sliders (except in + settings). [#458](https://github.com/keymapperorg/KeyMapper/issues/458) - The app intro slides will show feedback if the steps have been done correctly. ### Removed @@ -1326,15 +1601,22 @@ This sums up all the changes for 2.2 ### Bug Fixes -- Save and restore state for all view models. [#519](https://github.com/keymapperorg/KeyMapper/issues/519) -- Use View Binding in fragments properly. This should stop random crashes for some users. [#518](https://github.com/keymapperorg/KeyMapper/issues/518) -- Hold Down action option doesn't work for long press triggers. [#504](https://github.com/keymapperorg/KeyMapper/issues/504) +- Save and restore state for all view + models. [#519](https://github.com/keymapperorg/KeyMapper/issues/519) +- Use View Binding in fragments properly. This should stop random crashes for some + users. [#518](https://github.com/keymapperorg/KeyMapper/issues/518) +- Hold Down action option doesn't work for long press + triggers. [#504](https://github.com/keymapperorg/KeyMapper/issues/504) - A trigger for a specific device can still be detected if the same buttons on another device are pressed. [#523](https://github.com/keymapperorg/KeyMapper/issues/523) -- Fix layout of the trigger fragment on some screen sizes so that some things aren't cut off. [#522](https://github.com/keymapperorg/KeyMapper/issues/522) -- Remapping modifier keys to the same key didn't work as expected. [#563](https://github.com/keymapperorg/KeyMapper/issues/563) -- Parallel triggers which contained another parallel trigger didn't cancel the other. [#571](https://github.com/keymapperorg/KeyMapper/issues/571) -- Don't allow screen on/off constraints for fingerprint gestures [#570](https://github.com/keymapperorg/KeyMapper/issues/570) +- Fix layout of the trigger fragment on some screen sizes so that some things aren't cut + off. [#522](https://github.com/keymapperorg/KeyMapper/issues/522) +- Remapping modifier keys to the same key didn't work as + expected. [#563](https://github.com/keymapperorg/KeyMapper/issues/563) +- Parallel triggers which contained another parallel trigger didn't cancel the + other. [#571](https://github.com/keymapperorg/KeyMapper/issues/571) +- Don't allow screen on/off constraints for fingerprint + gestures [#570](https://github.com/keymapperorg/KeyMapper/issues/570) - Rename Key Mapper CI Keyboard to Key Mapper CI Basic Input Method. - Notifications had no icon on Android Lollipop. - remove coloured navigation bar on Android Lollipop. @@ -1342,12 +1624,16 @@ This sums up all the changes for 2.2 - Detecting whether remapping fingerprint gestures are supported didn't work. - The flashlight action would sometimes crash the app. - The error message for an app being disabled was the wrong one. -- Actions to open Android TV apps didn't work [#503](https://github.com/keymapperorg/KeyMapper/issues/503) -- The app list didn't show Android TV-only apps. [#487](https://github.com/keymapperorg/KeyMapper/issues/487) +- Actions to open Android TV apps didn't + work [#503](https://github.com/keymapperorg/KeyMapper/issues/503) +- The app list didn't show Android TV-only + apps. [#487](https://github.com/keymapperorg/KeyMapper/issues/487) - Settings for repeat rate and delay until repeat didn't match their names when configuring an action. -- Text would move up/down when sliding between slides in the app intro. [#540](https://github.com/keymapperorg/KeyMapper/issues/540) -- Icon for "specific app playing media" constraint had the wrong tint. [#535](https://github.com/keymapperorg/KeyMapper/issues/535) +- Text would move up/down when sliding between slides in the app + intro. [#540](https://github.com/keymapperorg/KeyMapper/issues/540) +- Icon for "specific app playing media" constraint had the wrong + tint. [#535](https://github.com/keymapperorg/KeyMapper/issues/535) - Limit Media actions to Android 4.4 KitKat+ because they don't work on older versions. - Up Key Event was sent from all keymaps with the "hold down" action option regardless of whether the trigger was released. [#533](https://github.com/keymapperorg/KeyMapper/issues/533) @@ -1362,15 +1648,18 @@ This sums up all the changes for 2.2 ### Added -- Remap fingerprint gestures! [#378](https://github.com/keymapperorg/KeyMapper/issues/378) Android 8.0+ and only on devices which support them. Even devices +- Remap fingerprint gestures! [#378](https://github.com/keymapperorg/KeyMapper/issues/378) Android + 8.0+ and only on devices which support them. Even devices with the setting to swipe down for notifications might not support this! The dev can't do anything about this. -- Show the fingerprint gesture intro slide when updating to 2.2 [#545](https://github.com/keymapperorg/KeyMapper/issues/545) +- Show the fingerprint gesture intro slide when updating to + 2.2 [#545](https://github.com/keymapperorg/KeyMapper/issues/545) - Show a silent notification, which advertises the remapping fingerprint gesture feature, when the user updates to 2.2 [#546](https://github.com/keymapperorg/KeyMapper/issues/546) - Trigger key maps from an Intent [#490](https://github.com/keymapperorg/KeyMapper/issues/490) - Prompt the user to go to https://dontkillmyapp.com when they first setup the app. -- Add Fdroid link to the Key Mapper GUI Keyboard ad. [#524](https://github.com/keymapperorg/KeyMapper/issues/524) +- Add Fdroid link to the Key Mapper GUI Keyboard + ad. [#524](https://github.com/keymapperorg/KeyMapper/issues/524) ### BREAKING CHANGES @@ -1387,9 +1676,12 @@ This sums up all the changes for 2.2 ### Bug Fixes -- Remapping modifier keys to the same key didn't work as expected. [#563](https://github.com/keymapperorg/KeyMapper/issues/563) -- Parallel triggers which contained another parallel trigger didn't cancel the other. [#571](https://github.com/keymapperorg/KeyMapper/issues/571) -- Don't allow screen on/off constraints for fingerprint gestures [#570](https://github.com/keymapperorg/KeyMapper/issues/570) +- Remapping modifier keys to the same key didn't work as + expected. [#563](https://github.com/keymapperorg/KeyMapper/issues/563) +- Parallel triggers which contained another parallel trigger didn't cancel the + other. [#571](https://github.com/keymapperorg/KeyMapper/issues/571) +- Don't allow screen on/off constraints for fingerprint + gestures [#570](https://github.com/keymapperorg/KeyMapper/issues/570) - Rename Key Mapper CI Keyboard to Key Mapper CI Basic Input Method. - Notifications had no icon on Android Lollipop. - remove coloured navigation bar on Android Lollipop. @@ -1397,12 +1689,16 @@ This sums up all the changes for 2.2 - Detecting whether remapping fingerprint gestures are supported didn't work. - The flashlight action would sometimes crash the app. - The error message for an app being disabled was the wrong one. -- Actions to open Android TV apps didn't work [#503](https://github.com/keymapperorg/KeyMapper/issues/503) -- The app list didn't show Android TV-only apps. [#487](https://github.com/keymapperorg/KeyMapper/issues/487) +- Actions to open Android TV apps didn't + work [#503](https://github.com/keymapperorg/KeyMapper/issues/503) +- The app list didn't show Android TV-only + apps. [#487](https://github.com/keymapperorg/KeyMapper/issues/487) - Settings for repeat rate and delay until repeat didn't match their names when configuring an action. -- Text would move up/down when sliding between slides in the app intro. [#540](https://github.com/keymapperorg/KeyMapper/issues/540) -- Icon for "specific app playing media" constraint had the wrong tint. [#535](https://github.com/keymapperorg/KeyMapper/issues/535) +- Text would move up/down when sliding between slides in the app + intro. [#540](https://github.com/keymapperorg/KeyMapper/issues/540) +- Icon for "specific app playing media" constraint had the wrong + tint. [#535](https://github.com/keymapperorg/KeyMapper/issues/535) - Limit Media actions to Android 4.4 KitKat+ because they don't work on older versions. - Up Key Event was sent from all keymaps with the "hold down" action option regardless of whether the trigger was released. [#533](https://github.com/keymapperorg/KeyMapper/issues/533) @@ -1417,7 +1713,8 @@ This sums up all the changes for 2.2 ### Added -- Remap fingerprint gestures! [#378](https://github.com/keymapperorg/KeyMapper/issues/378) Android 8.0+ and only on devices which support them. Even devices +- Remap fingerprint gestures! [#378](https://github.com/keymapperorg/KeyMapper/issues/378) Android + 8.0+ and only on devices which support them. Even devices with the setting to swipe down for notifications might not support this! The dev can't do anything about this. @@ -1426,26 +1723,37 @@ This sums up all the changes for 2.2 differentiated in Key Mapper lists. [#470](https://github.com/keymapperorg/KeyMapper/issues/470) - Show a warning at the top of the homescreen if the user hasn't disabled battery optimisation for Key Mapper. [#496](https://github.com/keymapperorg/KeyMapper/issues/496) -- Action option to hold down until the trigger is pressed again. [#479](https://github.com/keymapperorg/KeyMapper/issues/479) -- Action option to change the delay before the next action in the list. [#476](https://github.com/keymapperorg/KeyMapper/issues/476) +- Action option to hold down until the trigger is pressed + again. [#479](https://github.com/keymapperorg/KeyMapper/issues/479) +- Action option to change the delay before the next action in the + list. [#476](https://github.com/keymapperorg/KeyMapper/issues/476) - Orientation constraint. [#505](https://github.com/keymapperorg/KeyMapper/issues/505) -- Constraint for when a specific app is playing media. [#508](https://github.com/keymapperorg/KeyMapper/issues/508) -- Key Event action option to pretend that the Key Event came from a particular device. [#509](https://github.com/keymapperorg/KeyMapper/issues/509) -- Use duplicates of the same key in a sequence trigger. [#513](https://github.com/keymapperorg/KeyMapper/issues/513) -- Hold down repeatedly if repeat and hold down are enabled. [#500](https://github.com/keymapperorg/KeyMapper/issues/500) +- Constraint for when a specific app is playing + media. [#508](https://github.com/keymapperorg/KeyMapper/issues/508) +- Key Event action option to pretend that the Key Event came from a particular + device. [#509](https://github.com/keymapperorg/KeyMapper/issues/509) +- Use duplicates of the same key in a sequence + trigger. [#513](https://github.com/keymapperorg/KeyMapper/issues/513) +- Hold down repeatedly if repeat and hold down are + enabled. [#500](https://github.com/keymapperorg/KeyMapper/issues/500) ### Changes -- No max limit for sliders (except in settings). [#458](https://github.com/keymapperorg/KeyMapper/issues/458) +- No max limit for sliders (except in + settings). [#458](https://github.com/keymapperorg/KeyMapper/issues/458) ### Bug Fixes -- Save and restore state for all view models. [#519](https://github.com/keymapperorg/KeyMapper/issues/519) -- Use View Binding in fragments properly. This should stop random crashes for some users. [#518](https://github.com/keymapperorg/KeyMapper/issues/518) -- Hold Down action option doesn't work for long press triggers. [#504](https://github.com/keymapperorg/KeyMapper/issues/504) +- Save and restore state for all view + models. [#519](https://github.com/keymapperorg/KeyMapper/issues/519) +- Use View Binding in fragments properly. This should stop random crashes for some + users. [#518](https://github.com/keymapperorg/KeyMapper/issues/518) +- Hold Down action option doesn't work for long press + triggers. [#504](https://github.com/keymapperorg/KeyMapper/issues/504) - A trigger for a specific device can still be detected if the same buttons on another device are pressed. [#523](https://github.com/keymapperorg/KeyMapper/issues/523) -- Fix layout of the trigger fragment on some screen sizes so that some things aren't cut off. [#522](https://github.com/keymapperorg/KeyMapper/issues/522) +- Fix layout of the trigger fragment on some screen sizes so that some things aren't cut + off. [#522](https://github.com/keymapperorg/KeyMapper/issues/522) ## [2.1.0](https://github.com/sds100/KeyMapper/releases/tag/v2.1.0) @@ -1858,7 +2166,8 @@ This is the first release to be released on F-Droid. ### Bug Fix -- KEYCODE_BACK appeared twice in the keycode action list. [#247](https://github.com/keymapperorg/KeyMapper/issues/247) +- KEYCODE_BACK appeared twice in the keycode action + list. [#247](https://github.com/keymapperorg/KeyMapper/issues/247) ## [1.1.4](https://github.com/sds100/KeyMapper/releases/tag/v1.1.4) From ee72868915225e37a38ec073d04b3c3b5fd52da6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 9 Jul 2026 11:38:36 +0200 Subject: [PATCH 12/24] =?UTF-8?q?Add=20two=20new=20constraints=20=E2=80=94?= =?UTF-8?q?=20"Notification=20panel=20is=20showing"=20and=20"Notification?= =?UTF-8?q?=20panel=20is=20not=20showing"=20=E2=80=94=20that=20allow=20key?= =?UTF-8?q?=20maps=20to=20be=20conditional=20on=20whether=20the=20Android?= =?UTF-8?q?=20status=20bar=20/=20notification=20shade=20is=20expanded.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detection is implemented via the AccessibilityService window list: a TYPE_SYSTEM window whose root node belongs to com.android.systemui and is visible to the user is treated as evidence that the notification shade is expanded. This is a best-effort heuristic; accuracy may vary across OEMs and Android versions, as acknowledged in issue discussion. --- CHANGELOG.md | 4 ++++ .../constraints/ChooseConstraintViewModel.kt | 6 +++++ .../keymapper/base/constraints/Constraint.kt | 24 +++++++++++++++++++ .../base/constraints/ConstraintDependency.kt | 1 + .../base/constraints/ConstraintId.kt | 3 +++ .../base/constraints/ConstraintSnapshot.kt | 7 ++++++ .../base/constraints/ConstraintUiHelper.kt | 4 ++++ .../base/constraints/ConstraintUtils.kt | 16 +++++++++++++ .../constraints/DetectConstraintsUseCase.kt | 2 ++ .../KeyMapConstraintsComparator.kt | 2 ++ .../accessibility/BaseAccessibilityService.kt | 16 +++++++++++++ .../accessibility/IAccessibilityService.kt | 6 +++++ base/src/main/res/values/strings.xml | 3 +++ .../base/utils/TestConstraintSnapshot.kt | 3 +++ .../data/entities/ConstraintEntity.kt | 3 +++ 15 files changed, 100 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fba1b2f2a..115717397b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ #### 09 June 2026 +## Added + +- #2140 Add monochrome app icon layer for themed icon support on Android 13+. + ## Fixed - #2154 The expert mode debug screen is now only accessible after the expert mode warning has been diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index 4f41af9308..eb08042bab 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -277,6 +277,12 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.LOCK_SCREEN_NOT_SHOWING -> returnResult.emit(ConstraintData.LockScreenNotShowing) + ConstraintId.NOTIFICATION_PANEL_SHOWING -> + returnResult.emit(ConstraintData.NotificationPanelShowing) + + ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING -> + returnResult.emit(ConstraintData.NotificationPanelNotShowing) + ConstraintId.TIME -> { timeConstraintState = ConstraintData.Time( startHour = 0, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt index b855f57524..7b7fb41917 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt @@ -219,6 +219,16 @@ sealed class ConstraintData { override val id: ConstraintId = ConstraintId.HINGE_OPEN } + @Serializable + data object NotificationPanelShowing : ConstraintData() { + override val id: ConstraintId = ConstraintId.NOTIFICATION_PANEL_SHOWING + } + + @Serializable + data object NotificationPanelNotShowing : ConstraintData() { + override val id: ConstraintId = ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING + } + @Serializable data class Time( val startHour: Int, @@ -405,6 +415,10 @@ object ConstraintEntityMapper { ConstraintEntity.HINGE_CLOSED -> ConstraintData.HingeClosed ConstraintEntity.HINGE_OPEN -> ConstraintData.HingeOpen + ConstraintEntity.NOTIFICATION_PANEL_SHOWING -> ConstraintData.NotificationPanelShowing + ConstraintEntity.NOTIFICATION_PANEL_NOT_SHOWING -> + ConstraintData.NotificationPanelNotShowing + ConstraintEntity.TIME -> { val startTime = entity.extras.getData(ConstraintEntity.EXTRA_START_TIME).valueOrNull()!! @@ -725,6 +739,16 @@ object ConstraintEntityMapper { ConstraintEntity.HINGE_OPEN, ) + is ConstraintData.NotificationPanelShowing -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.NOTIFICATION_PANEL_SHOWING, + ) + + is ConstraintData.NotificationPanelNotShowing -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.NOTIFICATION_PANEL_NOT_SHOWING, + ) + is ConstraintData.Time -> ConstraintEntity( uid = constraint.uid, type = ConstraintEntity.TIME, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt index afab1a49ca..454a3324e9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt @@ -19,4 +19,5 @@ enum class ConstraintDependency { RINGER_MODE, CHARGING_STATE, HINGE_STATE, + NOTIFICATION_PANEL_STATE, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt index 1118a9d186..8f0512b696 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt @@ -64,5 +64,8 @@ enum class ConstraintId { HINGE_CLOSED, HINGE_OPEN, + NOTIFICATION_PANEL_SHOWING, + NOTIFICATION_PANEL_NOT_SHOWING, + TIME, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt index b568952133..5326f32129 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt @@ -79,6 +79,10 @@ class LazyConstraintSnapshot( lockScreenAdapter.isLockScreenShowing() } + private val isNotificationShadeExpanded: Boolean by lazy { + accessibilityService.isNotificationShadeExpanded.firstBlocking() + } + private val localTime = LocalTime.now() private fun isMediaPlaying(): Boolean { @@ -205,6 +209,9 @@ class LazyConstraintSnapshot( !isLockscreenShowing || appInForeground != "com.android.systemui" + is ConstraintData.NotificationPanelShowing -> isNotificationShadeExpanded + is ConstraintData.NotificationPanelNotShowing -> !isNotificationShadeExpanded + is ConstraintData.Time -> if (constraint.data.startTime.isAfter(constraint.data.endTime)) { localTime.isAfter(constraint.data.startTime) || diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt index 14fd7ed7a1..16a17570f9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt @@ -177,6 +177,10 @@ class ConstraintUiHelper( is ConstraintData.LockScreenNotShowing -> getString( R.string.constraint_lock_screen_not_showing, ) + is ConstraintData.NotificationPanelShowing -> + getString(R.string.constraint_notification_panel_showing) + is ConstraintData.NotificationPanelNotShowing -> + getString(R.string.constraint_notification_panel_not_showing) is ConstraintData.Time -> getString( R.string.constraint_time_formatted, arrayOf( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index 048854c7b6..3b4da0a763 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -24,6 +24,8 @@ import androidx.compose.material.icons.outlined.SignalWifiStatusbarNull import androidx.compose.material.icons.outlined.StayCurrentLandscape import androidx.compose.material.icons.outlined.StayCurrentPortrait import androidx.compose.material.icons.outlined.StopCircle +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.NotificationsOff import androidx.compose.material.icons.outlined.Timer import androidx.compose.material.icons.outlined.Vibration import androidx.compose.material.icons.outlined.Wifi @@ -117,6 +119,10 @@ object ConstraintUtils { ConstraintId.HINGE_OPEN, -> ConstraintCategory.DEVICE + ConstraintId.NOTIFICATION_PANEL_SHOWING, + ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING, + -> ConstraintCategory.DISPLAY + ConstraintId.TIME -> ConstraintCategory.TIME } @@ -200,6 +206,12 @@ object ConstraintUtils { Icons.Outlined.ScreenLockPortrait, ) ConstraintId.LOCK_SCREEN_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.LockOpen) + + ConstraintId.NOTIFICATION_PANEL_SHOWING -> + ComposeIconInfo.Vector(Icons.Outlined.Notifications) + ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING -> + ComposeIconInfo.Vector(Icons.Outlined.NotificationsOff) + ConstraintId.TIME -> ComposeIconInfo.Vector(Icons.Outlined.Timer) } @@ -254,6 +266,10 @@ object ConstraintUtils { ConstraintId.HINGE_OPEN -> R.string.constraint_hinge_open ConstraintId.LOCK_SCREEN_SHOWING -> R.string.constraint_lock_screen_showing ConstraintId.LOCK_SCREEN_NOT_SHOWING -> R.string.constraint_lock_screen_not_showing + ConstraintId.NOTIFICATION_PANEL_SHOWING -> + R.string.constraint_notification_panel_showing + ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING -> + R.string.constraint_notification_panel_not_showing ConstraintId.TIME -> R.string.constraint_time } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt index 23da542247..14e2da8297 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt @@ -105,6 +105,8 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( } ConstraintDependency.KEYBOARD_VISIBLE -> accessibilityService.isInputMethodVisible.map { dependency } + ConstraintDependency.NOTIFICATION_PANEL_STATE -> + accessibilityService.isNotificationShadeExpanded.map { dependency } } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt index ffbd85c775..ab4cb3dc31 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt @@ -144,6 +144,8 @@ class KeyMapConstraintsComparator( is ConstraintData.PhysicalOrientation -> Success( constraint.data.physicalOrientation.toString(), ) + ConstraintData.NotificationPanelShowing -> Success("") + ConstraintData.NotificationPanelNotShowing -> Success("") } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index de12b4600d..e26fd37fec 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -130,6 +130,13 @@ abstract class BaseAccessibilityService : override val isInputMethodVisible: Flow get() = _isInputMethodVisible + private val _isNotificationShadeExpanded by lazy { + MutableStateFlow(isNotificationShadeVisible()) + } + + override val isNotificationShadeExpanded: Flow + get() = _isNotificationShadeExpanded + override var serviceFlags: Int? get() = serviceInfo?.flags set(value) { @@ -295,6 +302,7 @@ abstract class BaseAccessibilityService : _activeWindowPackage.update { rootNode?.packageName?.toString() } _isInputMethodVisible.update { isImeWindowVisible() } + _isNotificationShadeExpanded.update { isNotificationShadeVisible() } } getController()?.onAccessibilityEvent(event) @@ -570,6 +578,14 @@ abstract class BaseAccessibilityService : return imeWindow != null && imeWindow.root?.isVisibleToUser == true } + private fun isNotificationShadeVisible(): Boolean { + return windows?.any { window -> + if (window.type != AccessibilityWindowInfo.TYPE_SYSTEM) return@any false + val root = window.root ?: return@any false + root.packageName?.toString() == "com.android.systemui" && root.isVisibleToUser + } ?: false + } + @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun injectText(text: String) { inputMethod?.currentInputConnection?.commitText( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt index df4cd6c0d9..eae36db432 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt @@ -63,6 +63,12 @@ interface IAccessibilityService : SwitchImeInterface { */ val isInputMethodVisible: Flow + /** + * Whether the notification shade (notification panel / status bar) is expanded. + * Detection is best-effort; accuracy may vary across OEMs and Android versions. + */ + val isNotificationShadeExpanded: Flow + fun findFocussedNode(focus: Int): AccessibilityNodeModel? @RequiresApi(Build.VERSION_CODES.TIRAMISU) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index da6128c88c..88b8ca418e 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -308,6 +308,9 @@ Hinge is closed Hinge is open + Notification panel is showing + Notification panel is not showing + Portrait (0°) Landscape (90°) Portrait (180°) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt index 89748d53df..ca6699b5c8 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt @@ -35,6 +35,7 @@ class TestConstraintSnapshot( val isLockscreenShowing: Boolean = false, val localTime: LocalTime = LocalTime.now(), val hingeState: HingeState = HingeState.Unavailable, + val isNotificationPanelShowing: Boolean = false, ) : ConstraintSnapshot { override fun isSatisfied(constraint: Constraint): Boolean { @@ -129,6 +130,8 @@ class TestConstraintSnapshot( hingeState is HingeState.Available && hingeState.isClosed() ConstraintData.HingeOpen -> hingeState is HingeState.Available && hingeState.isOpen() + ConstraintData.NotificationPanelShowing -> isNotificationPanelShowing + ConstraintData.NotificationPanelNotShowing -> !isNotificationPanelShowing } if (isSatisfied) { diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt index d81bdddddf..0a2c60feba 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt @@ -98,6 +98,9 @@ data class ConstraintEntity( const val HINGE_CLOSED = "hinge_closed" const val HINGE_OPEN = "hinge_open" + const val NOTIFICATION_PANEL_SHOWING = "notification_panel_showing" + const val NOTIFICATION_PANEL_NOT_SHOWING = "notification_panel_not_showing" + const val TIME = "time" const val EXTRA_PACKAGE_NAME = "extra_package_name" From 446de534e804fd1e6c2800b8a7eba30fd0a479ab Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:09:27 +0000 Subject: [PATCH 13/24] #1980 style: fix import ordering in ConstraintUtils.kt ktlint requires imports to be in lexicographic order; Notifications and NotificationsOff were appended after StopCircle instead of their correct position between MobileOff and PlayArrow. --- .../github/sds100/keymapper/base/constraints/ConstraintUtils.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index 3b4da0a763..daabcee482 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -24,8 +24,6 @@ import androidx.compose.material.icons.outlined.SignalWifiStatusbarNull import androidx.compose.material.icons.outlined.StayCurrentLandscape import androidx.compose.material.icons.outlined.StayCurrentPortrait import androidx.compose.material.icons.outlined.StopCircle -import androidx.compose.material.icons.outlined.Notifications -import androidx.compose.material.icons.outlined.NotificationsOff import androidx.compose.material.icons.outlined.Timer import androidx.compose.material.icons.outlined.Vibration import androidx.compose.material.icons.outlined.Wifi From dad0126bb2ecdcfccbc8e0ed7daebdd005be4574 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 9 Jul 2026 11:49:00 +0200 Subject: [PATCH 14/24] #1980 fix notification panel constraint detection --- .../accessibility/BaseAccessibilityService.kt | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index e26fd37fec..8e1af05ff5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -6,6 +6,7 @@ import android.accessibilityservice.GestureDescription import android.accessibilityservice.GestureDescription.StrokeDescription import android.accessibilityservice.InputMethod import android.app.ActivityManager +import android.app.KeyguardManager import android.content.Intent import android.content.res.Configuration import android.graphics.Path @@ -54,6 +55,8 @@ abstract class BaseAccessibilityService : IAccessibilityService, SavedStateRegistryOwner { + private val keyguardManager: KeyguardManager by lazy { getSystemService()!! } + @Inject lateinit var accessibilityServiceAdapterLazy: Lazy @@ -578,12 +581,29 @@ abstract class BaseAccessibilityService : return imeWindow != null && imeWindow.root?.isVisibleToUser == true } - private fun isNotificationShadeVisible(): Boolean { - return windows?.any { window -> - if (window.type != AccessibilityWindowInfo.TYPE_SYSTEM) return@any false - val root = window.root ?: return@any false - root.packageName?.toString() == "com.android.systemui" && root.isVisibleToUser - } ?: false + fun isNotificationShadeVisible( + isKeyguardLocked: Boolean = keyguardManager.isKeyguardLocked, + ): Boolean { + if (rootInActiveWindow?.packageName != "com.android.systemui") { + return false + } + + // Use a simple method of determining whether the status bar is expanded. If the system + // ui is showing and the the user is not on the lock screen the assume it is the status + // bar. The latter method commented out below isn't reliable and it is unique + // to every skin and version of Android, which leads to false positives. + if (isKeyguardLocked) { + // Assume the quick settings drawer is expanded if the brightness seekbar is showing. + val seekbar = rootInActiveWindow?.findAccessibilityNodeInfosByViewId( + "com.android.systemui:id/slider", + ) + // isVisibleToUser is required on Samsung S5e tablet. + ?.any { it.className == "android.widget.SeekBar" && it.isVisibleToUser } == true + + return seekbar + } + + return true } @RequiresApi(Build.VERSION_CODES.TIRAMISU) From 3409a69745c545244ad933560a856e93d4c96330 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 9 Jul 2026 11:50:44 +0200 Subject: [PATCH 15/24] fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13cb0db90c..57be1aaf55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - #2163 Add ringer mode constraints (Ring, Vibrate, Silent). - #2140 Add monochrome app icon layer for themed icon support on Android 13+. +- #2174 Add "Do not remap by default" preference to the default options settings page. ## [4.2.1](https://github.com/sds100/KeyMapper/releases/tag/v4.2.1) @@ -14,7 +15,6 @@ ## Added - #2140 Add monochrome app icon layer for themed icon support on Android 13+. -- #2174 Add "Do not remap by default" preference to the default options settings page. ## Fixed From 434ddc4bc0c98f4627752a23209bf40501e893f3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 9 Jul 2026 11:51:17 +0200 Subject: [PATCH 16/24] fix test --- .../sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt index c7c5ae89a5..8839dccb64 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -66,7 +66,7 @@ class ConfigTriggerDelegate { device: KeyEventTriggerDevice, requiresIme: Boolean, otherTriggerKeys: List = emptyList(), - doNotRemap: Boolean, + doNotRemap: Boolean = false, ): Trigger { val isPowerKey = KeyEventUtils.isPowerButtonKey(keyCode, scanCode) From 1da4938825e2919330272e1b7a275d15c915c813 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 09:20:23 +0000 Subject: [PATCH 17/24] #2184 feat: add display resolution constraint Add a new constraint that is satisfied when the device's real display resolution matches a chosen width and height. The constraint is configured in a bottom sheet that shows a row of selectable chips for each of the display's supported resolution modes, plus a "Custom" chip that reveals side-by-side width and height text fields. When the display reports one or zero supported modes, the text fields are shown immediately. The real display size is read from DisplayAdapter.size and compared ignoring orientation so the constraint holds in both portrait and landscape. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011GqGRM7xSwYjxKYT6gp66F --- CHANGELOG.md | 2 + .../constraints/ChooseConstraintScreen.kt | 1 + .../constraints/ChooseConstraintViewModel.kt | 18 ++ .../keymapper/base/constraints/Constraint.kt | 29 ++ .../base/constraints/ConstraintId.kt | 2 + .../base/constraints/ConstraintSnapshot.kt | 15 + .../base/constraints/ConstraintUiHelper.kt | 5 + .../base/constraints/ConstraintUtils.kt | 5 + .../constraints/CreateConstraintUseCase.kt | 10 + .../DisplayResolutionConstraintBottomSheet.kt | 292 ++++++++++++++++++ .../KeyMapConstraintsComparator.kt | 3 + base/src/main/res/values/strings.xml | 6 + .../base/utils/TestConstraintSnapshot.kt | 5 + .../data/entities/ConstraintEntity.kt | 5 + .../system/display/AndroidDisplayAdapter.kt | 8 + .../system/display/DisplayAdapter.kt | 6 + 16 files changed, 412 insertions(+) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 57be1aaf55..b73a3d80cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## Added +- #2184 Add a display resolution constraint. Pick from the display's supported resolutions or enter a + custom width and height. - #2163 Add ringer mode constraints (Ring, Vibrate, Silent). - #2140 Add monochrome app icon layer for themed icon support on Android 13+. - #2174 Add "Do not remap by default" preference to the default options settings page. diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt index 857f30330c..fb243994b2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt @@ -55,6 +55,7 @@ fun ChooseConstraintScreen(modifier: Modifier = Modifier, viewModel: ChooseConst val query by viewModel.searchQuery.collectAsStateWithLifecycle() TimeConstraintBottomSheet(viewModel) + DisplayResolutionConstraintBottomSheet(viewModel) ChooseConstraintScreen( modifier = modifier, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index eb08042bab..b5e3729ef8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -95,6 +95,8 @@ class ChooseConstraintViewModel @Inject constructor( var timeConstraintState: ConstraintData.Time? by mutableStateOf(null) + var displayResolutionState: DisplayResolutionSheetState? by mutableStateOf(null) + init { viewModelScope.launch { returnResult.collect { constraintData -> @@ -112,6 +114,13 @@ class ChooseConstraintViewModel @Inject constructor( } } + fun onDoneConfigDisplayResolutionClick(width: Int, height: Int) { + viewModelScope.launch { + returnResult.emit(ConstraintData.DisplayResolution(width = width, height = height)) + displayResolutionState = null + } + } + fun onNavigateBack() { viewModelScope.launch { popBackStack() @@ -152,6 +161,15 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff) + ConstraintId.DISPLAY_RESOLUTION -> { + val currentResolution = useCase.getCurrentResolution() + displayResolutionState = DisplayResolutionSheetState( + supportedResolutions = useCase.getSupportedResolutions(), + initialWidth = currentResolution.width, + initialHeight = currentResolution.height, + ) + } + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> returnResult.emit(ConstraintData.OrientationPortrait) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt index 7b7fb41917..f5d3c0ec57 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt @@ -105,6 +105,11 @@ sealed class ConstraintData { } } + @Serializable + data class DisplayResolution(val width: Int, val height: Int) : ConstraintData() { + override val id: ConstraintId = ConstraintId.DISPLAY_RESOLUTION + } + @Serializable data class FlashlightOn(val lens: CameraLens) : ConstraintData() { override val id: ConstraintId = ConstraintId.FLASHLIGHT_ON @@ -302,6 +307,12 @@ object ConstraintEntityMapper { return extraValue } + fun getResolutionWidth(): Int = + entity.extras.getData(ConstraintEntity.EXTRA_RESOLUTION_WIDTH).valueOrNull()!!.toInt() + + fun getResolutionHeight(): Int = + entity.extras.getData(ConstraintEntity.EXTRA_RESOLUTION_HEIGHT).valueOrNull()!!.toInt() + val constraintData = when (entity.type) { ConstraintEntity.APP_FOREGROUND -> ConstraintData.AppInForeground( getPackageName(), @@ -362,6 +373,11 @@ object ConstraintEntityMapper { ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED -> ConstraintData.PhysicalOrientation(PhysicalOrientation.LANDSCAPE_INVERTED) + ConstraintEntity.DISPLAY_RESOLUTION -> ConstraintData.DisplayResolution( + width = getResolutionWidth(), + height = getResolutionHeight(), + ) + ConstraintEntity.SCREEN_OFF -> ConstraintData.ScreenOff ConstraintEntity.SCREEN_ON -> ConstraintData.ScreenOn @@ -575,6 +591,19 @@ object ConstraintEntityMapper { ) } + is ConstraintData.DisplayResolution -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.DISPLAY_RESOLUTION, + EntityExtra( + ConstraintEntity.EXTRA_RESOLUTION_WIDTH, + constraint.data.width.toString(), + ), + EntityExtra( + ConstraintEntity.EXTRA_RESOLUTION_HEIGHT, + constraint.data.height.toString(), + ), + ) + is ConstraintData.ScreenOff -> ConstraintEntity( uid = constraint.uid, ConstraintEntity.SCREEN_OFF, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt index 8f0512b696..944eed883d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt @@ -31,6 +31,8 @@ enum class ConstraintId { PHYSICAL_ORIENTATION_PORTRAIT_INVERTED, PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED, + DISPLAY_RESOLUTION, + FLASHLIGHT_ON, FLASHLIGHT_OFF, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt index 5326f32129..0cc28dc291 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt @@ -5,6 +5,7 @@ import android.os.Build import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PhysicalOrientation +import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.camera.CameraAdapter @@ -51,6 +52,7 @@ class LazyConstraintSnapshot( displayAdapter.cachedPhysicalOrientation } private val isScreenOn: Boolean by lazy { displayAdapter.isScreenOn.firstBlocking() } + private val displaySize: SizeKM by lazy { displayAdapter.size } private val appsPlayingMedia: List by lazy { mediaAdapter.getActiveMediaSessionPackages() } @@ -134,6 +136,19 @@ class LazyConstraintSnapshot( is ConstraintData.ScreenOff -> !isScreenOn is ConstraintData.ScreenOn -> isScreenOn + + // Compare the resolution regardless of orientation so that the constraint + // holds whether the device is in portrait or landscape. + is ConstraintData.DisplayResolution -> + ( + displaySize.width == constraint.data.width && + displaySize.height == constraint.data.height + ) || + ( + displaySize.width == constraint.data.height && + displaySize.height == constraint.data.width + ) + is ConstraintData.FlashlightOff -> !cameraAdapter.isFlashlightOn(constraint.data.lens) is ConstraintData.FlashlightOn -> cameraAdapter.isFlashlightOn(constraint.data.lens) is ConstraintData.WifiConnected -> { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt index 16a17570f9..4990777498 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt @@ -105,6 +105,11 @@ class ConstraintUiHelper( is ConstraintData.ScreenOn -> getString(R.string.constraint_screen_on_description) + is ConstraintData.DisplayResolution -> getString( + R.string.constraint_display_resolution_description, + arrayOf(constraint.data.width, constraint.data.height), + ) + is ConstraintData.FlashlightOff -> if (constraint.data.lens == CameraLens.FRONT) { getString(R.string.constraint_front_flashlight_off_description) } else { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index daabcee482..6fae3f13f3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.base.constraints import androidx.annotation.StringRes import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AspectRatio import androidx.compose.material.icons.outlined.Battery2Bar import androidx.compose.material.icons.outlined.BatteryChargingFull import androidx.compose.material.icons.outlined.BluetoothConnected @@ -77,6 +78,7 @@ object ConstraintUtils { ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE, ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED, ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED, + ConstraintId.DISPLAY_RESOLUTION, -> ConstraintCategory.DISPLAY ConstraintId.FLASHLIGHT_ON, @@ -167,6 +169,8 @@ object ConstraintUtils { ConstraintId.SCREEN_OFF -> ComposeIconInfo.Vector(Icons.Outlined.MobileOff) ConstraintId.SCREEN_ON -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) + ConstraintId.DISPLAY_RESOLUTION -> ComposeIconInfo.Vector(Icons.Outlined.AspectRatio) + ConstraintId.FLASHLIGHT_OFF -> ComposeIconInfo.Vector(Icons.Outlined.FlashlightOff) ConstraintId.FLASHLIGHT_ON -> ComposeIconInfo.Vector(Icons.Outlined.FlashlightOn) @@ -225,6 +229,7 @@ object ConstraintUtils { R.string.constraint_choose_bluetooth_device_disconnected ConstraintId.SCREEN_ON -> R.string.constraint_choose_screen_on_description ConstraintId.SCREEN_OFF -> R.string.constraint_choose_screen_off_description + ConstraintId.DISPLAY_RESOLUTION -> R.string.constraint_choose_display_resolution ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> R.string.constraint_choose_orientation_portrait ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> R.string.constraint_choose_orientation_landscape diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt index 93b1d72b9d..aa83d41020 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt @@ -3,10 +3,12 @@ package io.github.sds100.keymapper.base.constraints import android.content.pm.PackageManager import android.os.Build import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.camera.CameraAdapter import io.github.sds100.keymapper.system.camera.CameraLens +import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.inputmethod.ImeInfo import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.network.NetworkAdapter @@ -20,6 +22,7 @@ class CreateConstraintUseCaseImpl @Inject constructor( private val inputMethodAdapter: InputMethodAdapter, private val preferenceRepository: PreferenceRepository, private val cameraAdapter: CameraAdapter, + private val displayAdapter: DisplayAdapter, ) : CreateConstraintUseCase { override fun isSupported(constraint: ConstraintId): KMError? { @@ -74,6 +77,10 @@ class CreateConstraintUseCaseImpl @Inject constructor( override fun getFlashlightLenses(): Set { return CameraLens.entries.filter { cameraAdapter.getFlashInfo(it) != null }.toSet() } + + override fun getSupportedResolutions(): List = displayAdapter.getSupportedResolutions() + + override fun getCurrentResolution(): SizeKM = displayAdapter.size } interface CreateConstraintUseCase { @@ -85,4 +92,7 @@ interface CreateConstraintUseCase { fun getSavedWifiSSIDs(): Flow> fun getFlashlightLenses(): Set + + fun getSupportedResolutions(): List + fun getCurrentResolution(): SizeKM } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt new file mode 100644 index 0000000000..8d8c789372 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt @@ -0,0 +1,292 @@ +package io.github.sds100.keymapper.base.constraints + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.common.utils.SizeKM +import kotlinx.coroutines.launch + +/** + * State passed to the display resolution bottom sheet. + * + * @param supportedResolutions the resolutions the display reports as supported. + * @param initialWidth the current real display width, used to prefill the custom fields. + * @param initialHeight the current real display height, used to prefill the custom fields. + */ +data class DisplayResolutionSheetState( + val supportedResolutions: List, + val initialWidth: Int, + val initialHeight: Int, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DisplayResolutionConstraintBottomSheet(viewModel: ChooseConstraintViewModel) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val state = viewModel.displayResolutionState ?: return + + DisplayResolutionConstraintBottomSheet( + sheetState = sheetState, + state = state, + onDismissRequest = { viewModel.displayResolutionState = null }, + onDoneClick = { width, height -> + viewModel.onDoneConfigDisplayResolutionClick(width, height) + }, + ) +} + +/** + * Compares two resolutions ignoring orientation so that e.g. 1080x1920 and 1920x1080 + * are treated as the same resolution. + */ +private fun SizeKM.matchesIgnoringOrientation(other: SizeKM): Boolean { + return (width == other.width && height == other.height) || + (width == other.height && height == other.width) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DisplayResolutionConstraintBottomSheet( + sheetState: SheetState, + state: DisplayResolutionSheetState, + onDismissRequest: () -> Unit = {}, + onDoneClick: (width: Int, height: Int) -> Unit = { _, _ -> }, +) { + val scope = rememberCoroutineScope() + + val initialSize = remember(state) { SizeKM(state.initialWidth, state.initialHeight) } + val matchingResolution = remember(state) { + state.supportedResolutions.firstOrNull { it.matchesIgnoringOrientation(initialSize) } + } + + // Show the text fields immediately when there is nothing meaningful to pick from + // or when the current resolution isn't one of the supported modes. + var isCustom by remember(state) { + mutableStateOf(state.supportedResolutions.size <= 1 || matchingResolution == null) + } + var selectedResolution by remember(state) { + mutableStateOf(matchingResolution ?: state.supportedResolutions.firstOrNull()) + } + var widthText by remember(state) { mutableStateOf(state.initialWidth.toString()) } + var heightText by remember(state) { mutableStateOf(state.initialHeight.toString()) } + + val customWidth = widthText.toIntOrNull() + val customHeight = heightText.toIntOrNull() + + val isValid = if (isCustom) { + customWidth != null && customWidth > 0 && customHeight != null && customHeight > 0 + } else { + selectedResolution != null + } + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + dragHandle = null, + ) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + textAlign = TextAlign.Center, + text = stringResource(R.string.constraint_display_resolution_bottom_sheet_title), + style = MaterialTheme.typography.headlineMedium, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // The chips are only useful when there is more than one supported mode to pick from. + if (state.supportedResolutions.size > 1) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + for (resolution in state.supportedResolutions) { + FilterChip( + selected = !isCustom && selectedResolution == resolution, + onClick = { + isCustom = false + selectedResolution = resolution + }, + label = { Text("${resolution.width} × ${resolution.height}") }, + ) + } + + FilterChip( + selected = isCustom, + onClick = { isCustom = true }, + label = { + Text( + stringResource( + R.string.constraint_display_resolution_custom_chip, + ), + ) + }, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } + + if (isCustom) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + OutlinedTextField( + modifier = Modifier.weight(1f), + value = widthText, + onValueChange = { widthText = it.filter(Char::isDigit) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + label = { + Text(stringResource(R.string.constraint_display_resolution_width)) + }, + ) + + OutlinedTextField( + modifier = Modifier.weight(1f), + value = heightText, + onValueChange = { heightText = it.filter(Char::isDigit) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + label = { + Text(stringResource(R.string.constraint_display_resolution_height)) + }, + ) + } + + 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() + } + }, + ) { + Text(stringResource(R.string.neg_cancel)) + } + + Spacer(modifier = Modifier.width(16.dp)) + + Button( + modifier = Modifier.weight(1f), + enabled = isValid, + onClick = { + val width = if (isCustom) customWidth else selectedResolution?.width + val height = if (isCustom) customHeight else selectedResolution?.height + + if (width != null && height != null) { + scope.launch { + sheetState.hide() + onDoneClick(width, height) + } + } + }, + ) { + Text(stringResource(R.string.pos_done)) + } + } + + Spacer(Modifier.height(16.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewWithModes() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + ) + + DisplayResolutionConstraintBottomSheet( + sheetState = sheetState, + state = DisplayResolutionSheetState( + supportedResolutions = listOf( + SizeKM(1080, 2400), + SizeKM(1440, 3200), + ), + initialWidth = 1080, + initialHeight = 2400, + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewCustomOnly() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + ) + + DisplayResolutionConstraintBottomSheet( + sheetState = sheetState, + state = DisplayResolutionSheetState( + supportedResolutions = emptyList(), + initialWidth = 1080, + initialHeight = 2400, + ), + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt index ab4cb3dc31..21cfdde574 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt @@ -115,6 +115,9 @@ class KeyMapConstraintsComparator( is ConstraintData.RingerMode -> Success("") is ConstraintData.ScreenOff -> Success("") is ConstraintData.ScreenOn -> Success("") + is ConstraintData.DisplayResolution -> Success( + "${constraint.data.width}x${constraint.data.height}", + ) is ConstraintData.WifiConnected -> if (constraint.data.ssid == null) { Success("") } else { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 3e6dbd8012..02f183468d 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -262,6 +262,12 @@ Physical: Landscape Physical: Portrait (upside down) Physical: Landscape (inverted) + Display resolution + Resolution is %1$d × %2$d + Display resolution + Custom + Width + Height App playing media App not playing media Media is playing diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt index ca6699b5c8..1af4cf04f2 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.base.constraints.ConstraintData import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PhysicalOrientation +import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.foldable.HingeState @@ -36,6 +37,7 @@ class TestConstraintSnapshot( val localTime: LocalTime = LocalTime.now(), val hingeState: HingeState = HingeState.Unavailable, val isNotificationPanelShowing: Boolean = false, + val displaySize: SizeKM = SizeKM(1080, 1920), ) : ConstraintSnapshot { override fun isSatisfied(constraint: Constraint): Boolean { @@ -72,6 +74,9 @@ class TestConstraintSnapshot( is ConstraintData.ScreenOff -> !isScreenOn is ConstraintData.ScreenOn -> isScreenOn + is ConstraintData.DisplayResolution -> + (displaySize.width == data.width && displaySize.height == data.height) || + (displaySize.width == data.height && displaySize.height == data.width) is ConstraintData.FlashlightOff -> when (data.lens) { CameraLens.BACK -> !isBackFlashlightOn CameraLens.FRONT -> !isFrontFlashlightOn diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt index 0a2c60feba..9e8a884ebe 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt @@ -65,6 +65,8 @@ data class ConstraintEntity( const val PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED = "constraint_physical_orientation_landscape_inverted" + const val DISPLAY_RESOLUTION = "constraint_display_resolution" + const val FLASHLIGHT_ON = "flashlight_on" const val FLASHLIGHT_OFF = "flashlight_off" @@ -111,6 +113,9 @@ data class ConstraintEntity( const val EXTRA_IME_ID = "extra_ime_id" const val EXTRA_IME_LABEL = "extra_ime_label" + const val EXTRA_RESOLUTION_WIDTH = "extra_resolution_width" + const val EXTRA_RESOLUTION_HEIGHT = "extra_resolution_height" + /** * The time is stored in the following format: 20:25. */ diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt index 560e60e42d..1c3a1d0e12 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt @@ -101,6 +101,14 @@ class AndroidDisplayAdapter @Inject constructor( override val size: SizeKM get() = ctx.getRealDisplaySize() + override fun getSupportedResolutions(): List { + val display = displayManager.displays.firstOrNull() ?: return emptyList() + + return display.supportedModes + .map { mode -> SizeKM(mode.physicalWidth, mode.physicalHeight) } + .distinct() + } + override val isAmbientDisplayEnabled: MutableStateFlow = MutableStateFlow(isAodEnabled()) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt index 377e148044..63ea6012ed 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt @@ -15,6 +15,12 @@ interface DisplayAdapter { val size: SizeKM val isAmbientDisplayEnabled: Flow + /** + * The distinct resolutions supported by the default display, taken from the + * display's supported modes. The dimensions are in the display's natural orientation. + */ + fun getSupportedResolutions(): List + fun isAutoRotateEnabled(): Boolean fun enableAutoRotate(): KMResult<*> fun disableAutoRotate(): KMResult<*> From c587e6ad9f8cffc79ef872d998992d060d375287 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 10 Jul 2026 11:29:40 +0200 Subject: [PATCH 18/24] fix changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57be1aaf55..417f2be66d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ ## Added - #2163 Add ringer mode constraints (Ring, Vibrate, Silent). -- #2140 Add monochrome app icon layer for themed icon support on Android 13+. - #2174 Add "Do not remap by default" preference to the default options settings page. ## [4.2.1](https://github.com/sds100/KeyMapper/releases/tag/v4.2.1) From 044b7ea32cc811580dbe7a0e230ca736b8135398 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 11:37:57 +0000 Subject: [PATCH 19/24] #2184 refactor: move display resolution constraint state into view model Hoist the bottom sheet's selection/custom/width/height state and the validity and resolved-resolution logic out of the composable and into ChooseConstraintViewModel. The bottom sheet is now a stateless view that renders DisplayResolutionSheetState and forwards user input through view model callbacks. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011GqGRM7xSwYjxKYT6gp66F --- .../constraints/ChooseConstraintViewModel.kt | 116 ++++++++++++++++-- .../DisplayResolutionConstraintBottomSheet.kt | 113 +++++------------ 2 files changed, 140 insertions(+), 89 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index b5e3729ef8..bf5ce3fd13 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -23,6 +23,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemModel import io.github.sds100.keymapper.base.utils.ui.showDialog import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PhysicalOrientation +import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.volume.RingerMode @@ -96,6 +97,7 @@ class ChooseConstraintViewModel @Inject constructor( var timeConstraintState: ConstraintData.Time? by mutableStateOf(null) var displayResolutionState: DisplayResolutionSheetState? by mutableStateOf(null) + private set init { viewModelScope.launch { @@ -114,9 +116,45 @@ class ChooseConstraintViewModel @Inject constructor( } } - fun onDoneConfigDisplayResolutionClick(width: Int, height: Int) { + fun onSelectDisplayResolution(resolution: SizeKM) { + displayResolutionState = displayResolutionState?.copy( + isCustom = false, + selectedResolution = resolution, + ) + } + + fun onSelectCustomDisplayResolution() { + displayResolutionState = displayResolutionState?.copy(isCustom = true) + } + + fun onDisplayResolutionWidthChange(width: String) { + displayResolutionState = displayResolutionState?.copy( + widthText = width.filter(Char::isDigit), + ) + } + + fun onDisplayResolutionHeightChange(height: String) { + displayResolutionState = displayResolutionState?.copy( + heightText = height.filter(Char::isDigit), + ) + } + + fun onDismissDisplayResolution() { + displayResolutionState = null + } + + fun onDoneConfigDisplayResolutionClick() { + val state = displayResolutionState ?: return + + val resolution = state.resolvedResolution ?: return + viewModelScope.launch { - returnResult.emit(ConstraintData.DisplayResolution(width = width, height = height)) + returnResult.emit( + ConstraintData.DisplayResolution( + width = resolution.width, + height = resolution.height, + ), + ) displayResolutionState = null } } @@ -162,12 +200,7 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff) ConstraintId.DISPLAY_RESOLUTION -> { - val currentResolution = useCase.getCurrentResolution() - displayResolutionState = DisplayResolutionSheetState( - supportedResolutions = useCase.getSupportedResolutions(), - initialWidth = currentResolution.width, - initialHeight = currentResolution.height, - ) + displayResolutionState = buildDisplayResolutionState() } ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> @@ -313,6 +346,25 @@ class ChooseConstraintViewModel @Inject constructor( } } + private fun buildDisplayResolutionState(): DisplayResolutionSheetState { + val supportedResolutions = useCase.getSupportedResolutions() + val currentResolution = useCase.getCurrentResolution() + + val matchingResolution = supportedResolutions.firstOrNull { + it.matchesIgnoringOrientation(currentResolution) + } + + return DisplayResolutionSheetState( + supportedResolutions = supportedResolutions, + // Show the text fields immediately when there is nothing meaningful to pick + // from or when the current resolution isn't one of the supported modes. + isCustom = supportedResolutions.size <= 1 || matchingResolution == null, + selectedResolution = matchingResolution ?: supportedResolutions.firstOrNull(), + widthText = currentResolution.width.toString(), + heightText = currentResolution.height.toString(), + ) + } + private suspend fun chooseFlashlightLens(): CameraLens? { val items = useCase.getFlashlightLenses().map { lens -> val label = when (lens) { @@ -616,3 +668,51 @@ class ChooseConstraintViewModel @Inject constructor( returnResult.emit(constraintData) } } + +/** + * State for the display resolution bottom sheet. + * + * @param supportedResolutions the resolutions the display reports as supported. + * @param isCustom whether the user is entering a custom resolution instead of picking a chip. + * @param selectedResolution the currently selected supported resolution, if any. + * @param widthText the custom width input. + * @param heightText the custom height input. + */ +data class DisplayResolutionSheetState( + val supportedResolutions: List, + val isCustom: Boolean, + val selectedResolution: SizeKM?, + val widthText: String, + val heightText: String, +) { + private val customWidth: Int? get() = widthText.toIntOrNull() + private val customHeight: Int? get() = heightText.toIntOrNull() + + /** + * The resolution that will be saved, or null when the current input is not valid. + */ + val resolvedResolution: SizeKM? + get() = if (isCustom) { + val width = customWidth + val height = customHeight + + if (width != null && width > 0 && height != null && height > 0) { + SizeKM(width, height) + } else { + null + } + } else { + selectedResolution + } + + val isValid: Boolean get() = resolvedResolution != null +} + +/** + * Compares two resolutions ignoring orientation so that e.g. 1080x1920 and 1920x1080 + * are treated as the same resolution. + */ +private fun SizeKM.matchesIgnoringOrientation(other: SizeKM): Boolean { + return (width == other.width && height == other.height) || + (width == other.height && height == other.width) +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt index 8d8c789372..5424ad8c9a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt @@ -21,11 +21,7 @@ import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -38,19 +34,6 @@ import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.common.utils.SizeKM import kotlinx.coroutines.launch -/** - * State passed to the display resolution bottom sheet. - * - * @param supportedResolutions the resolutions the display reports as supported. - * @param initialWidth the current real display width, used to prefill the custom fields. - * @param initialHeight the current real display height, used to prefill the custom fields. - */ -data class DisplayResolutionSheetState( - val supportedResolutions: List, - val initialWidth: Int, - val initialHeight: Int, -) - @OptIn(ExperimentalMaterial3Api::class) @Composable fun DisplayResolutionConstraintBottomSheet(viewModel: ChooseConstraintViewModel) { @@ -60,57 +43,29 @@ fun DisplayResolutionConstraintBottomSheet(viewModel: ChooseConstraintViewModel) DisplayResolutionConstraintBottomSheet( sheetState = sheetState, state = state, - onDismissRequest = { viewModel.displayResolutionState = null }, - onDoneClick = { width, height -> - viewModel.onDoneConfigDisplayResolutionClick(width, height) - }, + onSelectResolution = viewModel::onSelectDisplayResolution, + onSelectCustom = viewModel::onSelectCustomDisplayResolution, + onWidthChange = viewModel::onDisplayResolutionWidthChange, + onHeightChange = viewModel::onDisplayResolutionHeightChange, + onDismissRequest = viewModel::onDismissDisplayResolution, + onDoneClick = viewModel::onDoneConfigDisplayResolutionClick, ) } -/** - * Compares two resolutions ignoring orientation so that e.g. 1080x1920 and 1920x1080 - * are treated as the same resolution. - */ -private fun SizeKM.matchesIgnoringOrientation(other: SizeKM): Boolean { - return (width == other.width && height == other.height) || - (width == other.height && height == other.width) -} - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun DisplayResolutionConstraintBottomSheet( sheetState: SheetState, state: DisplayResolutionSheetState, + onSelectResolution: (SizeKM) -> Unit = {}, + onSelectCustom: () -> Unit = {}, + onWidthChange: (String) -> Unit = {}, + onHeightChange: (String) -> Unit = {}, onDismissRequest: () -> Unit = {}, - onDoneClick: (width: Int, height: Int) -> Unit = { _, _ -> }, + onDoneClick: () -> Unit = {}, ) { val scope = rememberCoroutineScope() - val initialSize = remember(state) { SizeKM(state.initialWidth, state.initialHeight) } - val matchingResolution = remember(state) { - state.supportedResolutions.firstOrNull { it.matchesIgnoringOrientation(initialSize) } - } - - // Show the text fields immediately when there is nothing meaningful to pick from - // or when the current resolution isn't one of the supported modes. - var isCustom by remember(state) { - mutableStateOf(state.supportedResolutions.size <= 1 || matchingResolution == null) - } - var selectedResolution by remember(state) { - mutableStateOf(matchingResolution ?: state.supportedResolutions.firstOrNull()) - } - var widthText by remember(state) { mutableStateOf(state.initialWidth.toString()) } - var heightText by remember(state) { mutableStateOf(state.initialHeight.toString()) } - - val customWidth = widthText.toIntOrNull() - val customHeight = heightText.toIntOrNull() - - val isValid = if (isCustom) { - customWidth != null && customWidth > 0 && customHeight != null && customHeight > 0 - } else { - selectedResolution != null - } - ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, @@ -140,18 +95,15 @@ private fun DisplayResolutionConstraintBottomSheet( ) { for (resolution in state.supportedResolutions) { FilterChip( - selected = !isCustom && selectedResolution == resolution, - onClick = { - isCustom = false - selectedResolution = resolution - }, + selected = !state.isCustom && state.selectedResolution == resolution, + onClick = { onSelectResolution(resolution) }, label = { Text("${resolution.width} × ${resolution.height}") }, ) } FilterChip( - selected = isCustom, - onClick = { isCustom = true }, + selected = state.isCustom, + onClick = onSelectCustom, label = { Text( stringResource( @@ -165,7 +117,7 @@ private fun DisplayResolutionConstraintBottomSheet( Spacer(modifier = Modifier.height(16.dp)) } - if (isCustom) { + if (state.isCustom) { Row( modifier = Modifier .fillMaxWidth() @@ -175,8 +127,8 @@ private fun DisplayResolutionConstraintBottomSheet( ) { OutlinedTextField( modifier = Modifier.weight(1f), - value = widthText, - onValueChange = { widthText = it.filter(Char::isDigit) }, + value = state.widthText, + onValueChange = onWidthChange, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { @@ -186,8 +138,8 @@ private fun DisplayResolutionConstraintBottomSheet( OutlinedTextField( modifier = Modifier.weight(1f), - value = heightText, - onValueChange = { heightText = it.filter(Char::isDigit) }, + value = state.heightText, + onValueChange = onHeightChange, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { @@ -222,16 +174,11 @@ private fun DisplayResolutionConstraintBottomSheet( Button( modifier = Modifier.weight(1f), - enabled = isValid, + enabled = state.isValid, onClick = { - val width = if (isCustom) customWidth else selectedResolution?.width - val height = if (isCustom) customHeight else selectedResolution?.height - - if (width != null && height != null) { - scope.launch { - sheetState.hide() - onDoneClick(width, height) - } + scope.launch { + sheetState.hide() + onDoneClick() } }, ) { @@ -262,8 +209,10 @@ private fun PreviewWithModes() { SizeKM(1080, 2400), SizeKM(1440, 3200), ), - initialWidth = 1080, - initialHeight = 2400, + isCustom = false, + selectedResolution = SizeKM(1080, 2400), + widthText = "1080", + heightText = "2400", ), ) } @@ -284,8 +233,10 @@ private fun PreviewCustomOnly() { sheetState = sheetState, state = DisplayResolutionSheetState( supportedResolutions = emptyList(), - initialWidth = 1080, - initialHeight = 2400, + isCustom = true, + selectedResolution = null, + widthText = "1080", + heightText = "2400", ), ) } From 2514706a0849121dde6bd466803e3a2ed6331ffd Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 12 Jul 2026 21:51:29 +0200 Subject: [PATCH 20/24] #2184 listen to changes in display resolutions --- .../constraints/ChooseConstraintViewModel.kt | 11 +- .../base/constraints/ConstraintDependency.kt | 1 + .../base/constraints/ConstraintUtils.kt | 154 ++++++++++++++++++ .../constraints/CreateConstraintUseCase.kt | 6 +- .../constraints/DetectConstraintsUseCase.kt | 17 ++ .../accessibility/BaseAccessibilityService.kt | 2 + .../system/display/AndroidDisplayAdapter.kt | 35 ++-- .../system/display/DisplayAdapter.kt | 3 +- 8 files changed, 213 insertions(+), 16 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index bf5ce3fd13..2367aa378f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -173,6 +173,7 @@ class ChooseConstraintViewModel @Inject constructor( onSelectDisplayOrientationConstraint() return@launch } + PHYSICAL_ORIENTATION_LIST_ITEM_ID -> { onSelectPhysicalOrientationConstraint() return@launch @@ -187,6 +188,7 @@ class ChooseConstraintViewModel @Inject constructor( -> onSelectAppConstraint(constraintType) ConstraintId.MEDIA_PLAYING -> returnResult.emit(ConstraintData.MediaPlaying) + ConstraintId.MEDIA_NOT_PLAYING -> returnResult.emit(ConstraintData.NoMediaPlaying) ConstraintId.BT_DEVICE_CONNECTED, @@ -268,6 +270,7 @@ class ChooseConstraintViewModel @Inject constructor( } ConstraintId.WIFI_ON -> returnResult.emit(ConstraintData.WifiOn) + ConstraintId.WIFI_OFF -> returnResult.emit(ConstraintData.WifiOff) ConstraintId.WIFI_CONNECTED, @@ -355,7 +358,7 @@ class ChooseConstraintViewModel @Inject constructor( } return DisplayResolutionSheetState( - supportedResolutions = supportedResolutions, + supportedResolutions = supportedResolutions.sortedBy { it.width }, // Show the text fields immediately when there is nothing meaningful to pick // from or when the current resolution isn't one of the supported modes. isCustom = supportedResolutions.size <= 1 || matchingResolution == null, @@ -406,15 +409,21 @@ class ChooseConstraintViewModel @Inject constructor( val constraintData = when (selectedOrientation) { ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ConstraintData.OrientationPortrait + ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ConstraintData.OrientationLandscape + ConstraintId.DISPLAY_ORIENTATION_0 -> ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_0) + ConstraintId.DISPLAY_ORIENTATION_90 -> ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_90) + ConstraintId.DISPLAY_ORIENTATION_180 -> ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_180) + ConstraintId.DISPLAY_ORIENTATION_270 -> ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_270) + else -> return } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt index 454a3324e9..ac326d39b4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt @@ -20,4 +20,5 @@ enum class ConstraintDependency { CHARGING_STATE, HINGE_STATE, NOTIFICATION_PANEL_STATE, + DISPLAY_RESOLUTIONS, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index 6fae3f13f3..cb9d2f2f79 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -134,11 +134,13 @@ object ConstraintUtils { -> ComposeIconInfo.Vector(Icons.Rounded.Android) ConstraintId.MEDIA_PLAYING -> ComposeIconInfo.Vector(Icons.Outlined.PlayArrow) + ConstraintId.MEDIA_NOT_PLAYING -> ComposeIconInfo.Vector(Icons.Outlined.StopCircle) ConstraintId.BT_DEVICE_CONNECTED -> ComposeIconInfo.Vector( Icons.Outlined.BluetoothConnected, ) + ConstraintId.BT_DEVICE_DISCONNECTED -> ComposeIconInfo.Vector( Icons.Outlined.BluetoothDisabled, ) @@ -154,6 +156,7 @@ object ConstraintUtils { ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ComposeIconInfo.Vector( Icons.Outlined.StayCurrentLandscape, ) + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ComposeIconInfo.Vector( Icons.Outlined.StayCurrentPortrait, ) @@ -167,18 +170,23 @@ object ConstraintUtils { -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentLandscape) ConstraintId.SCREEN_OFF -> ComposeIconInfo.Vector(Icons.Outlined.MobileOff) + ConstraintId.SCREEN_ON -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) ConstraintId.DISPLAY_RESOLUTION -> ComposeIconInfo.Vector(Icons.Outlined.AspectRatio) ConstraintId.FLASHLIGHT_OFF -> ComposeIconInfo.Vector(Icons.Outlined.FlashlightOff) + ConstraintId.FLASHLIGHT_ON -> ComposeIconInfo.Vector(Icons.Outlined.FlashlightOn) ConstraintId.WIFI_CONNECTED -> ComposeIconInfo.Vector(Icons.Outlined.Wifi) + ConstraintId.WIFI_DISCONNECTED -> ComposeIconInfo.Vector( Icons.Outlined.SignalWifiStatusbarNull, ) + ConstraintId.WIFI_OFF -> ComposeIconInfo.Vector(Icons.Outlined.WifiOff) + ConstraintId.WIFI_ON -> ComposeIconInfo.Vector(Icons.Outlined.Wifi) ConstraintId.IME_CHOSEN, @@ -186,31 +194,42 @@ object ConstraintUtils { -> ComposeIconInfo.Vector(Icons.Outlined.Keyboard) ConstraintId.KEYBOARD_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.Keyboard) + ConstraintId.KEYBOARD_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.KeyboardHide) ConstraintId.DEVICE_IS_LOCKED -> ComposeIconInfo.Vector(Icons.Outlined.Lock) + ConstraintId.DEVICE_IS_UNLOCKED -> ComposeIconInfo.Vector(Icons.Outlined.LockOpen) ConstraintId.IN_PHONE_CALL -> ComposeIconInfo.Vector(Icons.Outlined.Call) + ConstraintId.NOT_IN_PHONE_CALL -> ComposeIconInfo.Vector(Icons.Outlined.CallEnd) + ConstraintId.PHONE_RINGING -> ComposeIconInfo.Vector(Icons.Outlined.RingVolume) + ConstraintId.RINGER_MODE_NORMAL -> ComposeIconInfo.Vector(Icons.Outlined.Notifications) + ConstraintId.RINGER_MODE_VIBRATE -> ComposeIconInfo.Vector(Icons.Outlined.Vibration) + ConstraintId.RINGER_MODE_SILENT -> ComposeIconInfo.Vector(Icons.Outlined.NotificationsOff) ConstraintId.CHARGING -> ComposeIconInfo.Vector(Icons.Outlined.BatteryChargingFull) + ConstraintId.DISCHARGING -> ComposeIconInfo.Vector(Icons.Outlined.Battery2Bar) ConstraintId.HINGE_CLOSED -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) + ConstraintId.HINGE_OPEN -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentLandscape) ConstraintId.LOCK_SCREEN_SHOWING -> ComposeIconInfo.Vector( Icons.Outlined.ScreenLockPortrait, ) + ConstraintId.LOCK_SCREEN_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.LockOpen) ConstraintId.NOTIFICATION_PANEL_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.Notifications) + ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.NotificationsOff) @@ -219,60 +238,195 @@ object ConstraintUtils { fun getTitleStringId(constraintId: ConstraintId): Int = when (constraintId) { ConstraintId.APP_IN_FOREGROUND -> R.string.constraint_choose_app_foreground + ConstraintId.APP_NOT_IN_FOREGROUND -> R.string.constraint_choose_app_not_foreground + ConstraintId.APP_PLAYING_MEDIA -> R.string.constraint_choose_app_playing_media + ConstraintId.APP_NOT_PLAYING_MEDIA -> R.string.constraint_choose_app_not_playing_media + ConstraintId.MEDIA_NOT_PLAYING -> R.string.constraint_choose_media_not_playing + ConstraintId.MEDIA_PLAYING -> R.string.constraint_choose_media_playing + ConstraintId.BT_DEVICE_CONNECTED -> R.string.constraint_choose_bluetooth_device_connected + ConstraintId.BT_DEVICE_DISCONNECTED -> R.string.constraint_choose_bluetooth_device_disconnected + ConstraintId.SCREEN_ON -> R.string.constraint_choose_screen_on_description + ConstraintId.SCREEN_OFF -> R.string.constraint_choose_screen_off_description + ConstraintId.DISPLAY_RESOLUTION -> R.string.constraint_choose_display_resolution + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> R.string.constraint_choose_orientation_portrait + ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> R.string.constraint_choose_orientation_landscape + ConstraintId.DISPLAY_ORIENTATION_0 -> R.string.constraint_choose_orientation_0 + ConstraintId.DISPLAY_ORIENTATION_90 -> R.string.constraint_choose_orientation_90 + ConstraintId.DISPLAY_ORIENTATION_180 -> R.string.constraint_choose_orientation_180 + ConstraintId.DISPLAY_ORIENTATION_270 -> R.string.constraint_choose_orientation_270 + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT -> R.string.constraint_choose_physical_orientation_portrait + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE -> R.string.constraint_choose_physical_orientation_landscape + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED -> R.string.constraint_choose_physical_orientation_portrait_inverted + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED -> R.string.constraint_choose_physical_orientation_landscape_inverted + ConstraintId.FLASHLIGHT_ON -> R.string.constraint_flashlight_on + ConstraintId.FLASHLIGHT_OFF -> R.string.constraint_flashlight_off + ConstraintId.WIFI_ON -> R.string.constraint_wifi_on + ConstraintId.WIFI_OFF -> R.string.constraint_wifi_off + ConstraintId.WIFI_CONNECTED -> R.string.constraint_wifi_connected + ConstraintId.WIFI_DISCONNECTED -> R.string.constraint_wifi_disconnected + ConstraintId.IME_CHOSEN -> R.string.constraint_ime_chosen + ConstraintId.IME_NOT_CHOSEN -> R.string.constraint_ime_not_chosen + ConstraintId.KEYBOARD_SHOWING -> R.string.constraint_keyboard_showing + ConstraintId.KEYBOARD_NOT_SHOWING -> R.string.constraint_keyboard_not_showing + ConstraintId.DEVICE_IS_LOCKED -> R.string.constraint_device_is_locked + ConstraintId.DEVICE_IS_UNLOCKED -> R.string.constraint_device_is_unlocked + ConstraintId.IN_PHONE_CALL -> R.string.constraint_in_phone_call + ConstraintId.NOT_IN_PHONE_CALL -> R.string.constraint_not_in_phone_call + ConstraintId.PHONE_RINGING -> R.string.constraint_phone_ringing + ConstraintId.RINGER_MODE_NORMAL -> R.string.constraint_ringer_mode_normal + ConstraintId.RINGER_MODE_VIBRATE -> R.string.constraint_ringer_mode_vibrate + ConstraintId.RINGER_MODE_SILENT -> R.string.constraint_ringer_mode_silent + ConstraintId.CHARGING -> R.string.constraint_charging + ConstraintId.DISCHARGING -> R.string.constraint_discharging + ConstraintId.HINGE_CLOSED -> R.string.constraint_hinge_closed + ConstraintId.HINGE_OPEN -> R.string.constraint_hinge_open + ConstraintId.LOCK_SCREEN_SHOWING -> R.string.constraint_lock_screen_showing + ConstraintId.LOCK_SCREEN_NOT_SHOWING -> R.string.constraint_lock_screen_not_showing + ConstraintId.NOTIFICATION_PANEL_SHOWING -> R.string.constraint_notification_panel_showing + ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING -> R.string.constraint_notification_panel_not_showing + ConstraintId.TIME -> R.string.constraint_time } + + fun Constraint.getDependency(): Set { + return when (data) { + is ConstraintData.AppInForeground, + is ConstraintData.AppNotInForeground, + -> setOf(ConstraintDependency.FOREGROUND_APP) + + is ConstraintData.AppNotPlayingMedia, + is ConstraintData.AppPlayingMedia, + -> setOf(ConstraintDependency.APP_PLAYING_MEDIA) + + is ConstraintData.BtDeviceConnected, + is ConstraintData.BtDeviceDisconnected, + -> setOf(ConstraintDependency.CONNECTED_BT_DEVICES) + + is ConstraintData.Charging, + is ConstraintData.Discharging, + -> setOf(ConstraintDependency.CHARGING_STATE) + + is ConstraintData.DeviceIsLocked, + is ConstraintData.DeviceIsUnlocked, + -> setOf(ConstraintDependency.DEVICE_LOCKED_STATE) + + is ConstraintData.FlashlightOff, + is ConstraintData.FlashlightOn, + -> setOf(ConstraintDependency.FLASHLIGHT_STATE) + + is ConstraintData.ImeChosen, + is ConstraintData.ImeNotChosen, + -> setOf(ConstraintDependency.CHOSEN_IME) + + is ConstraintData.InPhoneCall, + is ConstraintData.NotInPhoneCall, + is ConstraintData.PhoneRinging, + -> setOf(ConstraintDependency.PHONE_STATE) + + is ConstraintData.MediaPlaying, + is ConstraintData.NoMediaPlaying, + -> setOf(ConstraintDependency.MEDIA_PLAYING) + + is ConstraintData.OrientationCustom, + is ConstraintData.OrientationLandscape, + is ConstraintData.OrientationPortrait, + -> setOf(ConstraintDependency.DISPLAY_ORIENTATION) + + is ConstraintData.ScreenOff, + is ConstraintData.ScreenOn, + -> setOf(ConstraintDependency.SCREEN_STATE) + + is ConstraintData.WifiConnected, + is ConstraintData.WifiDisconnected, + -> setOf(ConstraintDependency.WIFI_SSID) + + is ConstraintData.WifiOff, + is ConstraintData.WifiOn, + -> setOf(ConstraintDependency.WIFI_STATE) + + is ConstraintData.LockScreenShowing, + is ConstraintData.LockScreenNotShowing, + -> setOf(ConstraintDependency.LOCK_SCREEN_SHOWING) + + is ConstraintData.Time -> setOf( + ConstraintDependency.FOREGROUND_APP, + ConstraintDependency.SCREEN_STATE, + ) + + is ConstraintData.HingeClosed -> setOf(ConstraintDependency.HINGE_STATE) + + is ConstraintData.HingeOpen -> setOf(ConstraintDependency.HINGE_STATE) + + ConstraintData.KeyboardNotShowing, + ConstraintData.KeyboardShowing, + -> setOf(ConstraintDependency.KEYBOARD_VISIBLE) + + is ConstraintData.PhysicalOrientation -> setOf( + ConstraintDependency.PHYSICAL_ORIENTATION, + ) + + is ConstraintData.RingerMode -> setOf(ConstraintDependency.RINGER_MODE) + + ConstraintData.NotificationPanelNotShowing, ConstraintData.NotificationPanelShowing -> + setOf( + ConstraintDependency.NOTIFICATION_PANEL_STATE, + ) + + is ConstraintData.DisplayResolution -> setOf(ConstraintDependency.DISPLAY_RESOLUTIONS) + } + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt index aa83d41020..a9650ccbd5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt @@ -34,11 +34,13 @@ class CreateConstraintUseCaseImpl @Inject constructor( return KMError.SystemFeatureNotSupported(PackageManager.FEATURE_CAMERA_FLASH) } } + ConstraintId.HINGE_CLOSED, ConstraintId.HINGE_OPEN -> { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { return KMError.SdkVersionTooLow(Build.VERSION_CODES.R) } } + else -> Unit } @@ -78,7 +80,7 @@ class CreateConstraintUseCaseImpl @Inject constructor( return CameraLens.entries.filter { cameraAdapter.getFlashInfo(it) != null }.toSet() } - override fun getSupportedResolutions(): List = displayAdapter.getSupportedResolutions() + override fun getSupportedResolutions(): Set = displayAdapter.supportedResolutions.value override fun getCurrentResolution(): SizeKM = displayAdapter.size } @@ -93,6 +95,6 @@ interface CreateConstraintUseCase { fun getFlashlightLenses(): Set - fun getSupportedResolutions(): List + fun getSupportedResolutions(): Set fun getCurrentResolution(): SizeKM } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt index 14e2da8297..a2eb148de6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt @@ -63,6 +63,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ConstraintDependency.FOREGROUND_APP -> accessibilityService.activeWindowPackage.map { dependency } + ConstraintDependency.APP_PLAYING_MEDIA, ConstraintDependency.MEDIA_PLAYING -> merge( mediaAdapter.getActiveMediaSessionPackagesFlow(), @@ -71,11 +72,15 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ConstraintDependency.CONNECTED_BT_DEVICES -> devicesAdapter.connectedBluetoothDevices.map { dependency } + ConstraintDependency.SCREEN_STATE -> displayAdapter.isScreenOn.map { dependency } + ConstraintDependency.DISPLAY_ORIENTATION -> displayAdapter.orientation.map { dependency } + ConstraintDependency.PHYSICAL_ORIENTATION -> displayAdapter.physicalOrientation.map { dependency } + ConstraintDependency.FLASHLIGHT_STATE -> merge( cameraAdapter.isFlashlightOnFlow(CameraLens.FRONT), cameraAdapter.isFlashlightOnFlow(CameraLens.BACK), @@ -83,8 +88,11 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ConstraintDependency.WIFI_SSID -> networkAdapter.connectedWifiSSIDFlow.map { dependency } + ConstraintDependency.WIFI_STATE -> networkAdapter.isWifiEnabledFlow().map { dependency } + ConstraintDependency.CHOSEN_IME -> inputMethodAdapter.chosenIme.map { dependency } + ConstraintDependency.DEVICE_LOCKED_STATE -> lockScreenAdapter.isLockedFlow().map { dependency } @@ -95,18 +103,27 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ).map { dependency } ConstraintDependency.PHONE_STATE -> phoneAdapter.callStateFlow.map { dependency } + ConstraintDependency.RINGER_MODE -> volumeAdapter.ringerModeFlow.map { dependency } + ConstraintDependency.CHARGING_STATE -> powerAdapter.isCharging.map { dependency } + ConstraintDependency.HINGE_STATE -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { foldableAdapter.hingeState.map { dependency } } else { emptyFlow() } + ConstraintDependency.KEYBOARD_VISIBLE -> accessibilityService.isInputMethodVisible.map { dependency } + ConstraintDependency.NOTIFICATION_PANEL_STATE -> accessibilityService.isNotificationShadeExpanded.map { dependency } + + ConstraintDependency.DISPLAY_RESOLUTIONS -> displayAdapter.supportedResolutions.map { + dependency + } } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index 8e1af05ff5..1eda79100b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -294,6 +294,8 @@ abstract class BaseAccessibilityService : override fun onAccessibilityEvent(event: AccessibilityEvent?) { event ?: return + Timber.i("On Accessibility event: $event") + if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { // Catch exceptions because there is a crash report where // getRootInActivityWindow() fails internally inside the AccessibilityService. diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt index 1c3a1d0e12..48867bcfda 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt @@ -101,17 +101,12 @@ class AndroidDisplayAdapter @Inject constructor( override val size: SizeKM get() = ctx.getRealDisplaySize() - override fun getSupportedResolutions(): List { - val display = displayManager.displays.firstOrNull() ?: return emptyList() - - return display.supportedModes - .map { mode -> SizeKM(mode.physicalWidth, mode.physicalHeight) } - .distinct() - } - override val isAmbientDisplayEnabled: MutableStateFlow = MutableStateFlow(isAodEnabled()) + override val supportedResolutions: MutableStateFlow> = + MutableStateFlow(getSupportedResolutions()) + private val orientationEventListener = object : OrientationEventListener(ctx) { override fun onOrientationChanged(orientationDegrees: Int) { if (orientationDegrees == ORIENTATION_UNKNOWN) { @@ -127,15 +122,15 @@ class AndroidDisplayAdapter @Inject constructor( displayManager.registerDisplayListener( object : DisplayManager.DisplayListener { override fun onDisplayAdded(displayId: Int) { - _orientation.update { getDisplayOrientation() } + onDisplaysChanged() } override fun onDisplayRemoved(displayId: Int) { - _orientation.update { getDisplayOrientation() } + onDisplaysChanged() } override fun onDisplayChanged(displayId: Int) { - _orientation.update { getDisplayOrientation() } + onDisplaysChanged() // This listener API has lower latency than the broadcast receiver so also use this. val isScreenStateOn = displayManager.displays.first().state == Display.STATE_ON @@ -291,7 +286,6 @@ class AndroidDisplayAdapter @Inject constructor( Surface.ROTATION_90 -> Orientation.ORIENTATION_90 Surface.ROTATION_180 -> Orientation.ORIENTATION_180 Surface.ROTATION_270 -> Orientation.ORIENTATION_270 - else -> throw Exception("Don't know how to convert $sdkRotation to Orientation") } @@ -299,6 +293,11 @@ class AndroidDisplayAdapter @Inject constructor( return SettingsUtils.getSecureSetting(ctx, "doze_always_on") == 1 } + private fun onDisplaysChanged() { + _orientation.update { getDisplayOrientation() } + supportedResolutions.update { getSupportedResolutions() } + } + /** * Converts sensor orientation degrees to PhysicalOrientation. * @@ -321,13 +320,25 @@ class AndroidDisplayAdapter @Inject constructor( degrees >= (360 - ORIENTATION_TOLERANCE) || degrees < ORIENTATION_TOLERANCE -> PhysicalOrientation.PORTRAIT + degrees in (90 - ORIENTATION_TOLERANCE) until (90 + ORIENTATION_TOLERANCE) -> PhysicalOrientation.LANDSCAPE + degrees in (180 - ORIENTATION_TOLERANCE) until (180 + ORIENTATION_TOLERANCE) -> PhysicalOrientation.PORTRAIT_INVERTED + degrees in (270 - ORIENTATION_TOLERANCE) until (270 + ORIENTATION_TOLERANCE) -> PhysicalOrientation.LANDSCAPE_INVERTED + else -> _physicalOrientation.value // Keep current orientation in transition zone } } + + private fun getSupportedResolutions(): Set { + val display = displayManager.displays.firstOrNull() ?: return emptySet() + + return display.supportedModes + .map { mode -> SizeKM(mode.physicalWidth, mode.physicalHeight) } + .toSet() + } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt index 63ea6012ed..cec0291d52 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PhysicalOrientation import io.github.sds100.keymapper.common.utils.SizeKM import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow interface DisplayAdapter { val isScreenOn: Flow @@ -19,7 +20,7 @@ interface DisplayAdapter { * The distinct resolutions supported by the default display, taken from the * display's supported modes. The dimensions are in the display's natural orientation. */ - fun getSupportedResolutions(): List + val supportedResolutions: StateFlow> fun isAutoRotateEnabled(): Boolean fun enableAutoRotate(): KMResult<*> From 9bb6200ee5feaad2cd71070b4c3915880c259174 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 12 Jul 2026 22:02:52 +0200 Subject: [PATCH 21/24] update whats new --- base/src/main/assets/whats-new.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/src/main/assets/whats-new.txt b/base/src/main/assets/whats-new.txt index 35cd3d1f91..d64c53afb0 100644 --- a/base/src/main/assets/whats-new.txt +++ b/base/src/main/assets/whats-new.txt @@ -1,6 +1,6 @@ -• Talkback actions -• Monochrome app icon -• "Any input device" is the default for triggers -• Bug fixes and accessibility improvements +• Display resolution constraint +• Ringer mode constraint +• Set "do not remap" by default in settings +• Bug fixes 📖 View the complete changelog at: http://keymapper.app/changelog From ae9bba18b0a7bd7aab65fa9ac1270e232f04614f Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 13 Jul 2026 14:16:58 +0200 Subject: [PATCH 22/24] remove accidental accessibility event logging --- .../base/system/accessibility/BaseAccessibilityService.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index 1eda79100b..8e1af05ff5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -294,8 +294,6 @@ abstract class BaseAccessibilityService : override fun onAccessibilityEvent(event: AccessibilityEvent?) { event ?: return - Timber.i("On Accessibility event: $event") - if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { // Catch exceptions because there is a crash report where // getRootInActivityWindow() fails internally inside the AccessibilityService. From c7623f22cffbff08b406a7e449769c6367378c9f Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 13 Jul 2026 21:29:10 +0200 Subject: [PATCH 23/24] #2187 App constraints do not work with floating buttons Closes #2187 --- CHANGELOG.md | 5 ++++- .../BaseAccessibilityServiceController.kt | 15 +-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e39769c8ba..2555a723e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [4.3.0](https://github.com/sds100/KeyMapper/releases/tag/v4.3.0) -#### TO BE RELEASED +#### 13 July 2026 ## Added @@ -9,6 +9,9 @@ - #2163 Add ringer mode constraints (Ring, Vibrate, Silent). - #2174 Add "Do not remap by default" preference to the default options settings page. +## Fixed +- #2187 App constraints do not work with floating buttons + ## [4.2.1](https://github.com/sds100/KeyMapper/releases/tag/v4.2.1) #### 09 June 2026 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 8d6bef80fd..4ba31e2b9f 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 @@ -275,14 +275,6 @@ abstract class BaseAccessibilityServiceController( } } - // The accessibility event is only used on older than SDK 33. On newer versions the - // accessibility input method API is used. - val imeInputStartedEvents = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - AccessibilityEvent.TYPE_WINDOWS_CHANGED - } else { - 0 - } - val recordNodeEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED @@ -298,13 +290,8 @@ abstract class BaseAccessibilityServiceController( if (!changeImeOnInputFocus && recordState == RecordAccessibilityNodeState.Idle ) { - newEventTypes = - newEventTypes and (imeInputStartedEvents or recordNodeEvents).inv() + newEventTypes = newEventTypes and (recordNodeEvents).inv() } else { - if (changeImeOnInputFocus) { - newEventTypes = newEventTypes or imeInputStartedEvents - } - if (recordState is RecordAccessibilityNodeState.CountingDown) { newEventTypes = newEventTypes or recordNodeEvents } From ec7dbf8d7b222eb3b7277c39644161104e1cb696 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 13 Jul 2026 21:30:54 +0200 Subject: [PATCH 24/24] fastlane: check version code is incremented before building for production --- fastlane/Fastfile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7d694ae30b..dac41aeb1b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -29,6 +29,13 @@ lane :production do version_code = get_properties_value(key: "VERSION_CODE", path: "./app/version.properties") version_name = get_properties_value(key: "VERSION_NAME", path: "./app/version.properties") + live_version_codes = google_play_track_version_codes() + latest_live_version_code = live_version_codes.max || 0 + + if version_code.to_i <= latest_live_version_code + UI.user_error!("VERSION_CODE (#{version_code}) in app/version.properties must be greater than the latest closed track version code (#{latest_live_version_code}).") + end + # Don't create changelog for f-droid because not committing it # File.write("metadata/android/en-US/changelogs/" + version_code + ".txt", whats_new) @@ -96,6 +103,13 @@ lane :internal do version_code = get_properties_value(key: "VERSION_CODE", path: "./app/version.properties") version_name = get_properties_value(key: "VERSION_NAME", path: "./app/version.properties") + live_version_codes = google_play_track_version_codes(track: "alpha") + latest_live_version_code = live_version_codes.max || 0 + + if version_code.to_i <= latest_live_version_code + UI.user_error!("VERSION_CODE (#{version_code}) in app/version.properties must be greater than the latest closed track version code (#{latest_live_version_code}).") + end + # Check Rust formatting and run tests Dir.chdir("../evdev/src/main/rust/evdev_manager") do sh("cargo fmt --check")