From 248907c1488ca31ee4c719e309749c3512d5f3ab Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 00:16:01 +0000 Subject: [PATCH 01/30] #2140 feat: add monochrome app icon for themed icon support on Android 13+ Adds ic_launcher_monochrome.xml using evenOdd fill paths derived from the existing foreground icon so the DPad and keyboard shapes are recognisable in a single colour. Updates all adaptive-icon XML files (release, debug, ci) to reference the new monochrome layer. --- CHANGELOG.md | 4 ++++ .../ci/res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + .../res/drawable/ic_launcher_monochrome.xml | 24 +++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + 8 files changed, 34 insertions(+) create mode 100644 base/src/main/res/drawable/ic_launcher_monochrome.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index c0872a3650..f848f8fed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +## Added + +- #2140 Add monochrome app icon layer for themed icon support on Android 13+. + ## 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. diff --git a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd1fd..1084c24082 100644 --- a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd1fd..1084c24082 100644 --- a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd1fd..1084c24082 100644 --- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd1fd..1084c24082 100644 --- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/base/src/main/res/drawable/ic_launcher_monochrome.xml b/base/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000000..228496484e --- /dev/null +++ b/base/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd1fd..1084c24082 100644 --- a/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd1fd..1084c24082 100644 --- a/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file From 89d41916920fc736c44270c40e5df7dd5c92463e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 00:20:04 +0000 Subject: [PATCH 02/30] #1980 feat: add notification panel constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two new constraints — "Notification panel is showing" and "Notification panel is not showing" — that allow key maps to be conditional on whether the Android status bar / notification shade is expanded. 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 311a180edf..de6b9f3645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0) +## Added + +- #1980 Add "Notification panel is showing" and "Notification panel is not showing" constraints. Detection uses the accessibility service window list and is best-effort; accuracy may vary across OEMs and Android versions. + ## 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. 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..e8f9097058 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 @@ -267,6 +267,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 a2aa471fbd..bdd13d9a61 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 @@ -209,6 +209,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, @@ -388,6 +398,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()!! @@ -693,6 +707,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 4bbbcf1721..5b963be9b0 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 @@ -18,4 +18,5 @@ enum class ConstraintDependency { PHONE_STATE, 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 06463c6fd7..8de75c3ed2 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 @@ -60,5 +60,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 00ac3781ee..890c421b18 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 @@ -75,6 +75,10 @@ class LazyConstraintSnapshot( lockScreenAdapter.isLockScreenShowing() } + private val isNotificationShadeExpanded: Boolean by lazy { + accessibilityService.isNotificationShadeExpanded.firstBlocking() + } + private val localTime = LocalTime.now() private fun isMediaPlaying(): Boolean { @@ -199,6 +203,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 94b02aebc9..e3e91b0917 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 @@ -171,6 +171,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 80cd56c1c7..ac79fdd997 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 @@ -22,6 +22,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.Wifi import androidx.compose.material.icons.outlined.WifiOff @@ -111,6 +113,10 @@ object ConstraintUtils { ConstraintId.HINGE_OPEN, -> ConstraintCategory.DEVICE + ConstraintId.NOTIFICATION_PANEL_SHOWING, + ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING, + -> ConstraintCategory.DISPLAY + ConstraintId.TIME -> ConstraintCategory.TIME } @@ -191,6 +197,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) } @@ -242,6 +254,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 8bf75d6f24..e681fec045 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 @@ -101,6 +101,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 a48843773b..bec41c0573 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 @@ -143,6 +143,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 d247d41075..4f0d82e1fd 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 @@ -123,6 +123,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) { @@ -288,6 +295,7 @@ abstract class BaseAccessibilityService : _activeWindowPackage.update { rootNode?.packageName?.toString() } _isInputMethodVisible.update { isImeWindowVisible() } + _isNotificationShadeExpanded.update { isNotificationShadeVisible() } } getController()?.onAccessibilityEvent(event) @@ -563,6 +571,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 3fe19c6b92..9b58b9c1c1 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -305,6 +305,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 be77315302..e393f0e73c 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 @@ -33,6 +33,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 { @@ -126,6 +127,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 3deebac611..90426f8566 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 @@ -94,6 +94,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 d26633e702ebe544ec63c5e232d67a09501ef920 Mon Sep 17 00:00:00 2001 From: Jack Ambler <54366245+jambl3r@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:09:53 +0200 Subject: [PATCH 03/30] fix: Support email string formatting fix --- base/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 3fe19c6b92..e6561e8dc0 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1526,7 +1526,7 @@ Unsatisfied with Key Mapper To request a refund, please go to the Google Play Store app to find your payment receipt. Once you find the receipt, include the Transaction ID (starting with GPA) in this email.\n\nMy Transaction ID: Key Mapper Bug report - Please fill the following information so I can help you.\n\n1. Device model: %s\n2. Android version: %s\n3. Key Mapper version: %s\n4, Key maps (make a backup in the home screen menu)\n6. Screenshot of Key Mapper home screen\n6. Describe the problem you are having + Please fill the following information so I can help you.\n\n1. Device model: %s\n2. Android version: %s\n3. Key Mapper version: %s\n4. Key maps (make a backup in the home screen menu)\n5. Screenshot of Key Mapper home screen\n6. Describe the problem you are having 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. From 43c38b0ae60b38e2420231ad610c7854702c88c4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 3 Jun 2026 18:41:11 +0200 Subject: [PATCH 04/30] use correct monochrome foreground icon --- .../res/drawable/ic_launcher_monochrome.xml | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/base/src/main/res/drawable/ic_launcher_monochrome.xml b/base/src/main/res/drawable/ic_launcher_monochrome.xml index 228496484e..be1d8157f9 100644 --- a/base/src/main/res/drawable/ic_launcher_monochrome.xml +++ b/base/src/main/res/drawable/ic_launcher_monochrome.xml @@ -1,24 +1,58 @@ - - - - - + android:viewportWidth="24" + android:viewportHeight="24"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 801b7145b9106d4dae307e84c55c6e3970b047d7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 3 Jun 2026 19:50:48 +0200 Subject: [PATCH 05/30] bump version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index 0fa4ca5276..1d239ae17f 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ VERSION_NAME=4.2.0 -VERSION_CODE=252 +VERSION_CODE=253 From 7b260c4a80c119a8f7b59c2e624ab04e90799c16 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 3 Jun 2026 19:51:07 +0200 Subject: [PATCH 06/30] add changelog release date --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe73d8616..b4ca051484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0) +#### 03 June 2026 + ## Added - #2140 Add monochrome app icon layer for themed icon support on Android 13+. From f5a5ac3714e72a744d5b2a56b1b4020cdd95d438 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 4 Jun 2026 11:20:35 +0200 Subject: [PATCH 07/30] update whats-new --- base/src/main/assets/whats-new.txt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/base/src/main/assets/whats-new.txt b/base/src/main/assets/whats-new.txt index fab11d04bb..35cd3d1f91 100644 --- a/base/src/main/assets/whats-new.txt +++ b/base/src/main/assets/whats-new.txt @@ -1,9 +1,6 @@ -🎯 New Actions -• Select all text at the cursor -• Input on-screen keyboard enter/send button -• Show a toast message - -🔧 Improvements -• Bug fixes +• Talkback actions +• Monochrome app icon +• "Any input device" is the default for triggers +• Bug fixes and accessibility improvements 📖 View the complete changelog at: http://keymapper.app/changelog From ef0ae7833e52f2040af1276b14360725fc627d6d Mon Sep 17 00:00:00 2001 From: smh786 Date: Fri, 5 Jun 2026 20:43:40 +0100 Subject: [PATCH 08/30] Delay accessibility service init until user unlock --- .../accessibility/MyAccessibilityService.kt | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt index bc47591aae..1fdae9a795 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt @@ -1,6 +1,10 @@ package io.github.sds100.keymapper.system.accessibility import android.content.Intent +import android.os.UserManager +import android.view.KeyEvent +import android.view.accessibility.AccessibilityEvent +import androidx.core.content.getSystemService import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityService import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController @@ -14,6 +18,9 @@ class MyAccessibilityService : BaseAccessibilityService() { lateinit var controllerFactory: AccessibilityServiceController.Factory private var controller: AccessibilityServiceController? = null + private var loggedLockedInitDelay = false + + private val userManager: UserManager? by lazy { getSystemService() } override fun getController(): BaseAccessibilityServiceController? { return controller @@ -22,15 +29,47 @@ class MyAccessibilityService : BaseAccessibilityService() { override fun onServiceConnected() { super.onServiceConnected() + initializeControllerIfUserUnlocked() + } + + override fun onAccessibilityEvent(event: AccessibilityEvent?) { + if (!initializeControllerIfUserUnlocked()) { + return + } + + super.onAccessibilityEvent(event) + } + + override fun onKeyEvent(event: KeyEvent?): Boolean { + if (!initializeControllerIfUserUnlocked()) { + return false + } + + return super.onKeyEvent(event) + } + + private fun initializeControllerIfUserUnlocked(): Boolean { + if (userManager?.isUserUnlocked == false) { + if (!loggedLockedInitDelay) { + Timber.i("Accessibility service: Delay init because locked.") + loggedLockedInitDelay = true + } + + return false + } + + loggedLockedInitDelay = false + /* I would put this in onCreate but for some reason on some devices getting the application context would return null */ if (controller == null) { controller = controllerFactory.create(this) + controller?.onServiceConnected() } - controller?.onServiceConnected() + return true } override fun onUnbind(intent: Intent?): Boolean { From 47c620a3c4c5800da6075ac61c9ab9f0d6f7d081 Mon Sep 17 00:00:00 2001 From: smh786 Date: Fri, 5 Jun 2026 22:17:45 +0100 Subject: [PATCH 09/30] Fix Direct Boot accessibility startup --- app/src/main/AndroidManifest.xml | 4 +- .../sds100/keymapper/base/BaseKeyMapperApp.kt | 93 ++++++++++--------- .../keymapper/base/BootBroadcastReceiver.kt | 5 +- .../accessibility/BaseAccessibilityService.kt | 11 ++- 4 files changed, 63 insertions(+), 50 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d74d55e0eb..424e398db2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,6 @@ @@ -67,4 +67,4 @@ - \ No newline at end of file + diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index ee9e54e816..4bf021c4fd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -36,6 +36,7 @@ import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.permissions.Permission import java.util.Calendar import javax.inject.Inject +import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first @@ -51,46 +52,46 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { private val tag = BaseKeyMapperApp::class.simpleName @Inject - lateinit var appCoroutineScope: CoroutineScope + lateinit var appCoroutineScope: Lazy @Inject - lateinit var notificationController: NotificationController + lateinit var notificationController: Lazy @Inject - lateinit var packageManagerAdapter: AndroidPackageManagerAdapter + lateinit var packageManagerAdapter: Lazy @Inject - lateinit var devicesAdapter: AndroidDevicesAdapter + lateinit var devicesAdapter: Lazy @Inject - lateinit var permissionAdapter: AndroidPermissionAdapter + lateinit var permissionAdapter: Lazy @Inject - lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl + lateinit var accessibilityServiceAdapter: Lazy @Inject - lateinit var autoGrantPermissionController: AutoGrantPermissionController + lateinit var autoGrantPermissionController: Lazy @Inject - lateinit var loggingTree: KeyMapperLoggingTree + lateinit var loggingTree: Lazy @Inject - lateinit var settingsRepository: PreferenceRepositoryImpl + lateinit var settingsRepository: Lazy @Inject - lateinit var logRepository: LogRepository + lateinit var logRepository: Lazy @Inject - lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl + lateinit var keyEventRelayServiceWrapper: Lazy @Inject - lateinit var systemBridgeAutoStarter: SystemBridgeAutoStarter + lateinit var systemBridgeAutoStarter: Lazy @Inject - lateinit var systemBridgeConnectionManager: SystemBridgeConnectionManagerImpl + lateinit var systemBridgeConnectionManager: Lazy @Inject - lateinit var systemBridgeLogger: SystemBridgeLogger + lateinit var systemBridgeLogger: Lazy private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() } @@ -118,16 +119,18 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { Log.i(tag, "KeyMapperApp: OnCreate") Thread.setDefaultUncaughtExceptionHandler { thread, exception -> - // log in a blocking manner and always log regardless of whether the setting is turned on - val entry = LogEntryEntity( - id = 0, - time = Calendar.getInstance().timeInMillis, - severity = LogEntryEntity.SEVERITY_ERROR, - message = exception.stackTraceToString(), - ) - - runBlocking { - logRepository.insertSuspend(entry) + if (userManager?.isUserUnlocked != false) { + // log in a blocking manner and always log regardless of whether the setting is turned on + val entry = LogEntryEntity( + id = 0, + time = Calendar.getInstance().timeInMillis, + severity = LogEntryEntity.SEVERITY_ERROR, + message = exception.stackTraceToString(), + ) + + runBlocking { + logRepository.get().insertSuspend(entry) + } } priorExceptionHandler?.uncaughtException(thread, exception) @@ -168,7 +171,7 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { registerReceiver(broadcastReceiver, intentFilter) - settingsRepository.get(Keys.darkTheme) + settingsRepository.get().get(Keys.darkTheme) .map { it?.toIntOrNull() } .map { when (it) { @@ -178,15 +181,15 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { } } .onEach { mode -> AppCompatDelegate.setDefaultNightMode(mode) } - .launchIn(appCoroutineScope) + .launchIn(appCoroutineScope.get()) if (BuildConfig.BUILD_TYPE == "debug" || BuildConfig.BUILD_TYPE == "debug_release") { Timber.plant(Timber.DebugTree()) } - Timber.plant(loggingTree) + Timber.plant(loggingTree.get()) - notificationController.init() + notificationController.get().init() processLifecycleOwner.lifecycle.addObserver( object : LifecycleObserver { @@ -194,19 +197,19 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { // when the user returns to the app let everything know that the permissions could have changed - notificationController.onOpenApp() + notificationController.get().onOpenApp() if (BuildConfig.DEBUG && - permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS) + permissionAdapter.get().isGranted(Permission.WRITE_SECURE_SETTINGS) ) { - accessibilityServiceAdapter.start() + accessibilityServiceAdapter.get().start() } } }, ) - appCoroutineScope.launch { - notificationController.openApp.collectLatest { intentAction -> + appCoroutineScope.get().launch { + notificationController.get().openApp.collectLatest { intentAction -> Intent(this@BaseKeyMapperApp, getMainActivityClass()).apply { action = intentAction flags = Intent.FLAG_ACTIVITY_NEW_TASK @@ -216,38 +219,38 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { } } - notificationController.showToast.onEach { toast -> + notificationController.get().showToast.onEach { toast -> Toast.makeText(this, toast, Toast.LENGTH_SHORT).show() - }.launchIn(appCoroutineScope) + }.launchIn(appCoroutineScope.get()) - autoGrantPermissionController.start() - keyEventRelayServiceWrapper.bind() + autoGrantPermissionController.get().start() + keyEventRelayServiceWrapper.get().bind() - if (systemBridgeConnectionManager.isConnected()) { + if (systemBridgeConnectionManager.get().isConnected()) { Timber.i("KeyMapperApp: System bridge is connected") } else { Timber.i("KeyMapperApp: System bridge is disconnected") } - systemBridgeAutoStarter.init() + systemBridgeAutoStarter.get().init() // Initialize SystemBridgeLogger to start receiving log messages from SystemBridge. // Using Lazy<> to avoid circular dependency issues and ensure it's only created // when the API level requirement is met. - systemBridgeLogger.start() + systemBridgeLogger.get().start() - appCoroutineScope.launch { - systemBridgeConnectionManager.connectionState.collect { state -> + appCoroutineScope.get().launch { + systemBridgeConnectionManager.get().connectionState.collect { state -> if (state is SystemBridgeConnectionState.Connected) { val isUsed = - settingsRepository.get(Keys.isSystemBridgeUsed).first() ?: false + settingsRepository.get().get(Keys.isSystemBridgeUsed).first() ?: false // Enable the setting to use PRO mode for key event actions the first time they use PRO mode. if (!isUsed) { - settingsRepository.set(Keys.keyEventActionsUseSystemBridge, true) + settingsRepository.get().set(Keys.keyEventActionsUseSystemBridge, true) } - settingsRepository.set(Keys.isSystemBridgeUsed, true) + settingsRepository.get().set(Keys.isSystemBridgeUsed, true) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt b/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt index 7bb321b372..3d45969541 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt @@ -15,10 +15,13 @@ class BootBroadcastReceiver : BroadcastReceiver() { Timber.i( "Boot completed broadcast: time since boot = ${SystemClock.elapsedRealtime() / 1000}", ) + (context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked() } Intent.ACTION_LOCKED_BOOT_COMPLETED -> { - (context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked() + Timber.i( + "Locked boot completed broadcast: time since boot = ${SystemClock.elapsedRealtime() / 1000}", + ) } } } 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 d247d41075..ba78fa0632 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 @@ -28,6 +28,7 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint +import dagger.Lazy import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType import io.github.sds100.keymapper.base.actions.talkback.TalkbackGesturePerformer @@ -54,10 +55,16 @@ abstract class BaseAccessibilityService : SavedStateRegistryOwner { @Inject - lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl + lateinit var accessibilityServiceAdapterLazy: Lazy + + val accessibilityServiceAdapter: AccessibilityServiceAdapterImpl + get() = accessibilityServiceAdapterLazy.get() @Inject - lateinit var inputMethodAdapter: InputMethodAdapter + lateinit var inputMethodAdapterLazy: Lazy + + val inputMethodAdapter: InputMethodAdapter + get() = inputMethodAdapterLazy.get() private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) private var savedStateRegistryController: SavedStateRegistryController? = From af57c6eceec914cd0bf9df5df1efffb59fe81f2a Mon Sep 17 00:00:00 2001 From: smh786 Date: Sat, 6 Jun 2026 20:03:33 +0100 Subject: [PATCH 10/30] #2153 fix: add Direct Boot changelog entry --- CHANGELOG.md | 1 + .../java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt | 2 +- .../base/system/accessibility/BaseAccessibilityService.kt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ca051484..cbac66a102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## Fixed +- #2153 Prevent Direct Boot startup from initializing credential-encrypted app storage before the user unlocks. - #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 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index 4bf021c4fd..50c1a77345 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDexApplication +import dagger.Lazy import io.github.sds100.keymapper.base.expertmode.SystemBridgeAutoStarter import io.github.sds100.keymapper.base.logging.KeyMapperLoggingTree import io.github.sds100.keymapper.base.logging.SystemBridgeLogger @@ -36,7 +37,6 @@ import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.permissions.Permission import java.util.Calendar import javax.inject.Inject -import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first 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 ba78fa0632..de12b4600d 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 @@ -27,8 +27,8 @@ import androidx.lifecycle.LifecycleRegistry import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner -import dagger.hilt.android.AndroidEntryPoint import dagger.Lazy +import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType import io.github.sds100.keymapper.base.actions.talkback.TalkbackGesturePerformer From 0f66372652363aaa5de949b2242719dff5a97c34 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 00:11:58 +0000 Subject: [PATCH 11/30] #2154 fix: gate expert mode debug screen on warning acknowledgement Move the debugging section (GetEvent button) inside the `warningState is Understood` conditional so it is only visible after the user acknowledges the expert mode warning, preventing the bypass via the debug screen. Also add a defensive guard in ExpertModeViewModel.onGetEventClick() that returns early if the warning has not been acknowledged. --- CHANGELOG.md | 1 + .../base/expertmode/ExpertModeScreen.kt | 42 +++++++++---------- .../base/expertmode/ExpertModeViewModel.kt | 1 + 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ca051484..fd65797b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## Fixed +- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged. - #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 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt index eb1e4c1e8b..d843f45bc1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt @@ -252,6 +252,27 @@ private fun Content( ) } } + + Spacer(modifier = Modifier.height(16.dp)) + + OptionsHeaderRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + icon = Icons.Outlined.BugReport, + text = stringResource(R.string.settings_section_debugging_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + modifier = Modifier.padding(horizontal = 8.dp), + title = stringResource(R.string.title_pref_get_event_debug), + text = stringResource(R.string.summary_pref_get_event_debug), + icon = Icons.Outlined.BugReport, + onClick = onGetEventClick, + ) + Spacer(modifier = Modifier.height(8.dp)) } else { Text( modifier = Modifier.padding(horizontal = 32.dp), @@ -259,27 +280,6 @@ private fun Content( textAlign = TextAlign.Center, ) } - - Spacer(modifier = Modifier.height(16.dp)) - - OptionsHeaderRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - icon = Icons.Outlined.BugReport, - text = stringResource(R.string.settings_section_debugging_title), - ) - - Spacer(modifier = Modifier.height(8.dp)) - - OptionPageButton( - modifier = Modifier.padding(horizontal = 8.dp), - title = stringResource(R.string.title_pref_get_event_debug), - text = stringResource(R.string.summary_pref_get_event_debug), - icon = Icons.Outlined.BugReport, - onClick = onGetEventClick, - ) - Spacer(modifier = Modifier.height(8.dp)) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt index 7130911276..ecc4da5437 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt @@ -190,6 +190,7 @@ class ExpertModeViewModel @Inject constructor( fun onGetEventClick() { viewModelScope.launch { + if (warningState.value !is ExpertModeWarningState.Understood) return@launch navigate("get_event_debug", NavDestination.GetEvent) } } From bb6b4368ba357f7e1b27339e273b57c44a33ae7f Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 09:57:41 +0200 Subject: [PATCH 12/30] bump version to 4.2.1 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index 1d239ae17f..1813e00437 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.2.0 -VERSION_CODE=253 +VERSION_NAME=4.2.1 +VERSION_CODE=254 From 6a7a7520be78bdc6557e65fc08d7172e7c3fceaf Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 10:37:19 +0200 Subject: [PATCH 13/30] #2156 fix: do not throw an error when the Talkback application can not be found because there are many different package names out there Closes #2156 --- CHANGELOG.md | 1 + .../sds100/keymapper/base/actions/ActionErrorSnapshot.kt | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd65797b61..0feee9d04e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged. - #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. +- #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there. ## Changed diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt index 4dcc63aa74..6acbd9374d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt @@ -49,6 +49,7 @@ class LazyActionErrorSnapshot( cameraAdapter, permissionAdapter, ) { + private val keyMapperImeHelper = KeyMapperImeHelper(switchImeInterface, inputMethodAdapter, buildConfigProvider.packageName) @@ -265,10 +266,6 @@ class LazyActionErrorSnapshot( } } - is ActionData.TalkBackGesture -> { - return getAppError(TALKBACK_PACKAGE_NAME) - } - else -> {} } @@ -321,5 +318,3 @@ interface ActionErrorSnapshot { fun getError(action: ActionData): KMError? fun getErrors(actions: List): Map } - -private const val TALKBACK_PACKAGE_NAME = "com.google.android.marvin.talkback" From 8816dd9170aca97a9818217e496147f020c6324b Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 10:45:22 +0200 Subject: [PATCH 14/30] fix changelog --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0feee9d04e..1547eebca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [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. + ## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0) #### 03 June 2026 @@ -8,9 +17,7 @@ ## Fixed -- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged. - #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. -- #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there. ## Changed From aed7ca324f53f6aff24355b375b7bbd090cb4e33 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 09:09:07 +0000 Subject: [PATCH 15/30] #2157 feat: use settings ContentProvider via system bridge for choose setting screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the system bridge is connected (expert mode), the choose setting screen now queries the Settings ContentProvider directly through a new getAllSettings() AIDL method instead of falling back to the restricted ContentProvider query from the app process. The system bridge runs with shell/root privileges, giving it access to all settings rows that the app process cannot see. A queryCompat helper is added to IContentProviderUtils to handle the version-dependent IContentProvider.query() signatures across API 29–34+. When the system bridge is not connected, the existing ContentResolver query is used as a fallback. --- CHANGELOG.md | 1 + .../base/actions/ChooseSettingViewModel.kt | 39 +++++++++++- .../keymapper/sysbridge/ISystemBridge.aidl | 7 +++ .../sysbridge/service/SystemBridge.kt | 63 +++++++++++++++++++ .../sysbridge/utils/IContentProviderUtils.kt | 26 ++++++++ 5 files changed, 135 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7299123be3..a2cb693525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - #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) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt index 2e3c343042..e8e7ad3522 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt @@ -7,6 +7,9 @@ import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.isConnected import io.github.sds100.keymapper.system.settings.SettingType import io.github.sds100.keymapper.system.settings.SettingsAdapter import javax.inject.Inject @@ -18,12 +21,14 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @HiltViewModel class ChooseSettingViewModel @Inject constructor( private val settingsAdapter: SettingsAdapter, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, @@ -36,7 +41,7 @@ class ChooseSettingViewModel @Inject constructor( val selectedSettingType = MutableStateFlow(SettingType.SYSTEM) val settings: StateFlow>> = combine(selectedSettingType, searchQuery) { type, query -> - val allSettings = settingsAdapter.getAll(type) + val allSettings = getSettings(type) val items = allSettings .filter { (key, _) -> query == null || key.contains(query, ignoreCase = true) } @@ -46,6 +51,38 @@ class ChooseSettingViewModel @Inject constructor( }.flowOn(Dispatchers.Default) .stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + private suspend fun getSettings(type: SettingType): Map { + if (systemBridgeConnectionManager.isConnected()) { + val namespace = when (type) { + SettingType.SYSTEM -> "system" + SettingType.SECURE -> "secure" + SettingType.GLOBAL -> "global" + } + val result = runInterruptible(Dispatchers.IO) { + systemBridgeConnectionManager.run { bridge -> + bridge.getAllSettings(namespace) + } + } + if (result is Success && result.value.isNotEmpty()) { + return parseKeyValuePairs(result.value) + } + } + return settingsAdapter.getAll(type) + } + + private fun parseKeyValuePairs(pairs: Array): Map { + val settings = sortedMapOf() + for (entry in pairs) { + val eqIdx = entry.indexOf('=') + if (eqIdx < 0) continue + val key = entry.substring(0, eqIdx) + if (key.isBlank()) continue + val value = if (eqIdx < entry.length - 1) entry.substring(eqIdx + 1) else null + settings[key] = value + } + return settings + } + fun onNavigateBack() { viewModelScope.launch { popBackStack() diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl index ee04b124c8..8477527153 100644 --- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -53,4 +53,11 @@ interface ISystemBridge { void registerLogCallback(ILogCallback callback) = 23; void unregisterLogCallback() = 24; void setLogLevel(int level) = 25; + + /** + * Returns all settings for the given namespace as an array of "key=value" strings. + * The namespace must be one of "system", "secure", or "global". + * Queries the Settings ContentProvider directly with the system bridge's elevated privileges. + */ + String[] getAllSettings(String namespace) = 26; } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index 888958cc41..ee375ebf9a 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -903,4 +903,67 @@ class SystemBridge : ISystemBridge.Stub() { override fun setLogLevel(level: Int) { setLogLevelNative(level) } + + override fun getAllSettings(namespace: String?): Array { + namespace ?: return emptyArray() + + val settingsUri = when (namespace) { + "system" -> android.provider.Settings.System.CONTENT_URI + "secure" -> android.provider.Settings.Secure.CONTENT_URI + "global" -> android.provider.Settings.Global.CONTENT_URI + else -> return emptyArray() + } + + val authority = "settings" + val token: android.os.IBinder? = null + val userId = UserHandleUtils.getCallingUserId() + var provider: android.content.IContentProvider? = null + + try { + provider = ActivityManagerApis.getContentProviderExternal( + authority, + userId, + token, + authority, + ) + + if (provider == null) { + Log.w(TAG, "getAllSettings: Settings content provider is null for namespace=$namespace") + return emptyArray() + } + + val cursor = IContentProviderUtils.queryCompat( + provider, + processPackageName, + settingsUri, + arrayOf("name", "value"), + null, + ) ?: return emptyArray() + + val results = mutableListOf() + cursor.use { + val nameIndex = it.getColumnIndex("name") + val valueIndex = it.getColumnIndex("value") + if (nameIndex >= 0) { + while (it.moveToNext()) { + val name = it.getString(nameIndex) ?: continue + val value = if (valueIndex >= 0) it.getString(valueIndex) else null + results.add("$name=${value ?: ""}") + } + } + } + return results.toTypedArray() + } catch (e: Exception) { + Log.e(TAG, "getAllSettings: Failed to query settings for namespace=$namespace", e) + return emptyArray() + } finally { + if (provider != null) { + try { + ActivityManagerApis.removeContentProviderExternal(authority, token) + } catch (tr: Throwable) { + Log.w(TAG, "getAllSettings: Failed to remove content provider", tr) + } + } + } + } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt index d83d20744d..23ded26d92 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt @@ -2,6 +2,8 @@ package io.github.sds100.keymapper.sysbridge.utils import android.content.AttributionSource import android.content.IContentProvider +import android.database.Cursor +import android.net.Uri import android.os.Build import android.os.Bundle @@ -38,4 +40,28 @@ internal object IContentProviderUtils { return result } + + @Throws(android.os.RemoteException::class) + fun queryCompat( + provider: IContentProvider, + callingPkg: String?, + url: Uri, + projection: Array?, + queryArgs: Bundle?, + ): Cursor? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val uid = android.system.Os.getuid() + provider.query( + AttributionSource.Builder(uid).setPackageName(callingPkg).build(), + url, + projection, + queryArgs, + null, + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + provider.query(callingPkg, null as String?, url, projection, queryArgs, null) + } else { + provider.query(callingPkg, url, projection, queryArgs, null) + } + } } From 1b2a0771c663cf7b46f1e1c6bf204d2efe0a3bd7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 11:11:38 +0200 Subject: [PATCH 16/30] cleanup code --- .../keymapper/sysbridge/service/SystemBridge.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index ee375ebf9a..652135af7e 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -481,7 +481,7 @@ class SystemBridge : ISystemBridge.Stub() { override fun setGrabTargets( devices: Array?, - ): Array? { + ): Array { return setGrabTargetsNative(devices?.filterNotNull()?.toTypedArray() ?: emptyArray()) } @@ -915,9 +915,9 @@ class SystemBridge : ISystemBridge.Stub() { } val authority = "settings" - val token: android.os.IBinder? = null + val token: IBinder? = null val userId = UserHandleUtils.getCallingUserId() - var provider: android.content.IContentProvider? = null + var provider: IContentProvider? = null try { provider = ActivityManagerApis.getContentProviderExternal( @@ -928,7 +928,10 @@ class SystemBridge : ISystemBridge.Stub() { ) if (provider == null) { - Log.w(TAG, "getAllSettings: Settings content provider is null for namespace=$namespace") + Log.w( + TAG, + "getAllSettings: Settings content provider is null for namespace=$namespace", + ) return emptyArray() } From bf4b09b910518bec7c4d9a1981d00277a51d4091 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 9 Jun 2026 11:26:59 +0200 Subject: [PATCH 17/30] add proguard rules for android.os.ICancellationSignal --- app/proguard-rules.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 1e905036f2..0f7be6ae53 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -162,6 +162,7 @@ # android.content package classes -keep class android.content.IContentProvider** { *; } -keep class android.content.IIntentReceiver** { *; } +-keep class android.os.ICancellationSignal** { *; } # android.content.pm package classes -keep class android.content.pm.IPackageManager** { *; } @@ -245,3 +246,4 @@ -dontwarn android.view.IWindowManager** -dontwarn com.android.internal.app.** -dontwarn com.android.internal.policy.** +-dontwarn android.os.ICancellationSignal \ No newline at end of file From 77eb149eb9b417308f6fc52a3aa8dc9be1ea4193 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 11 Jun 2026 10:47:29 +0200 Subject: [PATCH 18/30] bump version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index 1813e00437..0e1183310c 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ VERSION_NAME=4.2.1 -VERSION_CODE=254 +VERSION_CODE=256 From 61e9c722651c0b560e31e9afacc8fd0b6be11432 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:09:27 +0000 Subject: [PATCH 19/30] #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. --- .../sds100/keymapper/base/constraints/ConstraintUtils.kt | 4 ++-- 1 file changed, 2 insertions(+), 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 ac79fdd997..b6e1eec2a7 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,8 @@ 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.PlayArrow import androidx.compose.material.icons.outlined.RingVolume import androidx.compose.material.icons.outlined.ScreenLockPortrait @@ -22,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.Wifi import androidx.compose.material.icons.outlined.WifiOff From 0c88cd6637fd9430271906fa5df469aaf7a5058f Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 24 Jun 2026 10:28:30 +0200 Subject: [PATCH 20/30] 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 21/30] 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 22/30] #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 f3398a61dc39f3749dff5ba1fc92ef9219cc46e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 12:20:57 +0000 Subject: [PATCH 23/30] #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 24/30] #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 25/30] #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 26/30] #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 7ad06237b6d02564355577f1d4602e5d462b3e52 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 9 Jul 2026 11:31:49 +0200 Subject: [PATCH 27/30] 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 45d1778f89d8739215bf5bd6f740d39a4a51050e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 00:20:04 +0000 Subject: [PATCH 28/30] #1980 feat: add notification panel constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two new constraints — "Notification panel is showing" and "Notification panel is not showing" — that allow key maps to be conditional on whether the Android status bar / notification shade is expanded. 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 | 20 ++++++++++------ .../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, 109 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4d637bbf0..1b6b568eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,20 @@ +## [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). +- #1980 Add "Notification panel is showing" and "Notification panel is not showing" constraints. Detection uses the accessibility service window list and is best-effort; accuracy may vary across OEMs and Android versions. + ## [4.2.1](https://github.com/sds100/KeyMapper/releases/tag/v4.2.1) #### 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 acknowledged. @@ -11,13 +24,6 @@ ## [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. 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 14b5fd2df52b2272b4ba310d68765e23f873dd09 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:09:27 +0000 Subject: [PATCH 29/30] #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 90fae206c7902155a3008ec3a98da77d90cbb4c5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 9 Jul 2026 11:36:05 +0200 Subject: [PATCH 30/30] fix changelog --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 377835a58a..1b6b568eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,10 +24,6 @@ ## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0) -## Added - -- #1980 Add "Notification panel is showing" and "Notification panel is not showing" constraints. Detection uses the accessibility service window list and is best-effort; accuracy may vary across OEMs and Android versions. - ## 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.