diff options
3 files changed, 171 insertions, 6 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 75e04841d4b0..e4a7813ed918 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -16,18 +16,25 @@ package com.android.systemui.statusbar.notification.interruption +import android.app.NotificationManager.IMPORTANCE_HIGH import android.database.ContentObserver import android.hardware.display.AmbientDisplayConfiguration import android.os.Handler +import android.os.PowerManager import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED import android.provider.Settings.Global.HEADS_UP_OFF import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.StatusBarState.SHADE +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.time.SystemClock class PeekDisabledSuppressor( private val globalSettings: GlobalSettings, @@ -88,3 +95,67 @@ class PulseBatterySaverSuppressor(private val batteryController: BatteryControll ) { override fun shouldSuppress() = batteryController.isAodPowerSave() } + +class PeekPackageSnoozedSuppressor(private val headsUpManager: HeadsUpManager) : + VisualInterruptionFilter(types = setOf(PEEK), reason = "package snoozed") { + override fun shouldSuppress(entry: NotificationEntry) = + when { + // Assume any notification with an FSI is time-sensitive (like an alarm or incoming + // call) and ignore whether HUNs have been snoozed for the package. + entry.sbn.notification.fullScreenIntent != null -> false + + // Otherwise, check if the package is snoozed. + else -> headsUpManager.isSnoozed(entry.sbn.packageName) + } +} + +class PeekAlreadyBubbledSuppressor(private val statusBarStateController: StatusBarStateController) : + VisualInterruptionFilter(types = setOf(PEEK), reason = "already bubbled") { + override fun shouldSuppress(entry: NotificationEntry) = + when { + statusBarStateController.state != SHADE -> false + else -> entry.isBubble + } +} + +class PeekDndSuppressor() : + VisualInterruptionFilter(types = setOf(PEEK), reason = "suppressed by DND") { + override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressPeek() +} + +class PeekNotImportantSuppressor() : + VisualInterruptionFilter(types = setOf(PEEK), reason = "not important") { + override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_HIGH +} + +class PeekDeviceNotInUseSuppressor( + private val powerManager: PowerManager, + private val statusBarStateController: StatusBarStateController +) : VisualInterruptionCondition(types = setOf(PEEK), reason = "not in use") { + override fun shouldSuppress() = + when { + !powerManager.isScreenOn || statusBarStateController.isDreaming -> true + else -> false + } +} + +class PeekOldWhenSuppressor(private val systemClock: SystemClock) : + VisualInterruptionFilter(types = setOf(PEEK), reason = "old when") { + private fun whenAge(entry: NotificationEntry) = + systemClock.currentTimeMillis() - entry.sbn.notification.`when` + + override fun shouldSuppress(entry: NotificationEntry): Boolean = + when { + // Ignore a "when" of 0, as it is unlikely to be a meaningful timestamp. + entry.sbn.notification.`when` <= 0L -> false + + // Assume all HUNs with FSIs, foreground services, or user-initiated jobs are + // time-sensitive, regardless of their "when". + entry.sbn.notification.fullScreenIntent != null || + entry.sbn.notification.isForegroundService || + entry.sbn.notification.isUserInitiatedJob -> false + + // Otherwise, check if the HUN's "when" is too old. + else -> whenAge(entry) >= MAX_HUN_WHEN_AGE_MS + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index bae713486be5..f0cbd214937b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -56,6 +56,12 @@ constructor( addCondition(PeekDisabledSuppressor(globalSettings, headsUpManager, logger, mainHandler)) addCondition(PulseDisabledSuppressor(ambientDisplayConfiguration, userTracker)) addCondition(PulseBatterySaverSuppressor(batteryController)) + addFilter(PeekPackageSnoozedSuppressor(headsUpManager)) + addFilter(PeekAlreadyBubbledSuppressor(statusBarStateController)) + addFilter(PeekDndSuppressor()) + addFilter(PeekNotImportantSuppressor()) + addCondition(PeekDeviceNotInUseSuppressor(powerManager, statusBarStateController)) + addFilter(PeekOldWhenSuppressor(systemClock)) started = true } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt index 551119436c7f..11be85c70428 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt @@ -19,9 +19,11 @@ package com.android.systemui.statusbar.notification.interruption import android.app.ActivityManager import android.app.Notification import android.app.Notification.BubbleMetadata +import android.app.Notification.FLAG_BUBBLE import android.app.NotificationChannel import android.app.NotificationManager.IMPORTANCE_DEFAULT import android.app.NotificationManager.IMPORTANCE_HIGH +import android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK import android.app.NotificationManager.VISIBILITY_NO_OVERRIDE import android.app.PendingIntent import android.app.PendingIntent.FLAG_MUTABLE @@ -43,9 +45,11 @@ import com.android.systemui.statusbar.FakeStatusBarStateController import com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.StatusBarState.SHADE +import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.KeyguardStateController @@ -127,6 +131,84 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } @Test + fun testShouldNotPeek_packageSnoozed() { + ensurePeekState { hunSnoozed = true } + assertShouldNotHeadsUp(buildPeekEntry()) + } + + @Test + fun testShouldPeek_packageSnoozedButFsi() { + ensurePeekState { hunSnoozed = true } + assertShouldHeadsUp(buildFsiEntry()) + } + + @Test + fun testShouldNotPeek_alreadyBubbled() { + ensurePeekState { statusBarState = SHADE } + assertShouldNotHeadsUp(buildPeekEntry { isBubble = true }) + } + + @Test + fun testShouldPeek_isBubble_shadeLocked() { + ensurePeekState { statusBarState = SHADE_LOCKED } + assertShouldHeadsUp(buildPeekEntry { isBubble = true }) + } + + @Test + fun testShouldPeek_isBubble_keyguard() { + ensurePeekState { statusBarState = KEYGUARD } + assertShouldHeadsUp(buildPeekEntry { isBubble = true }) + } + + @Test + fun testShouldNotPeek_dnd() { + ensurePeekState() + assertShouldNotHeadsUp(buildPeekEntry { suppressedVisualEffects = SUPPRESSED_EFFECT_PEEK }) + } + + @Test + fun testShouldNotPeek_notImportant() { + ensurePeekState() + assertShouldNotHeadsUp(buildPeekEntry { importance = IMPORTANCE_DEFAULT }) + } + + @Test + fun testShouldNotPeek_screenOff() { + ensurePeekState { isScreenOn = false } + assertShouldNotHeadsUp(buildPeekEntry()) + } + + @Test + fun testShouldNotPeek_dreaming() { + ensurePeekState { isDreaming = true } + assertShouldNotHeadsUp(buildPeekEntry()) + } + + @Test + fun testShouldNotPeek_oldWhen() { + ensurePeekState() + assertShouldNotHeadsUp(buildPeekEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS) }) + } + + @Test + fun testShouldPeek_notQuiteOldEnoughWhen() { + ensurePeekState() + assertShouldHeadsUp(buildPeekEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS - 1) }) + } + + @Test + fun testShouldPeek_zeroWhen() { + ensurePeekState() + assertShouldHeadsUp(buildPeekEntry { whenMs = 0L }) + } + + @Test + fun testShouldPeek_oldWhenButFsi() { + ensurePeekState() + assertShouldHeadsUp(buildFsiEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS) }) + } + + @Test fun testShouldPeek_defaultLegacySuppressor() { ensurePeekState() provider.addLegacySuppressor(neverSuppresses) @@ -380,6 +462,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { var visibilityOverride: Int? = null var hasFsi = false var canBubble: Boolean? = null + var isBubble = false var hasBubbleMetadata = false var bubbleSuppressNotification: Boolean? = null @@ -413,6 +496,11 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } } .build() + .apply { + if (isBubble) { + flags = flags or FLAG_BUBBLE + } + } .let { NotificationEntryBuilder().setNotification(it) } .apply { setPkg(TEST_PACKAGE) @@ -449,18 +537,18 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { run(block) } - private fun buildFsiEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { - importance = IMPORTANCE_HIGH - hasFsi = true - run(block) - } - private fun buildBubbleEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { canBubble = true hasBubbleMetadata = true run(block) } + private fun buildFsiEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { + importance = IMPORTANCE_HIGH + hasFsi = true + run(block) + } + private fun whenAgo(whenAgeMs: Long) = systemClock.currentTimeMillis() - whenAgeMs } |