From 89d41916920fc736c44270c40e5df7dd5c92463e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 00:20:04 +0000 Subject: [PATCH] #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"