Merge "Implement FSI suppression logic" into main
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
new file mode 100644
index 0000000..6af2543
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.interruption
+
+import android.app.NotificationManager.IMPORTANCE_HIGH
+import android.os.PowerManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_DEVICE_DREAMING
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_DEVICE_NOT_INTERACTIVE
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_DEVICE_NOT_PROVISIONED
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_KEYGUARD_OCCLUDED
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_KEYGUARD_SHOWING
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_LOCKED_SHADE
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_EXPECTED_TO_HUN
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_NOT_IMPORTANT_ENOUGH
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_NO_FULL_SCREEN_INTENT
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_NO_HUN_OR_KEYGUARD
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_PACKAGE_SUSPENDED
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SHOW_STICKY_HUN
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSED_BY_DND
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSED_ONLY_BY_DND
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA
+import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+
+class FullScreenIntentDecisionProvider(
+ private val deviceProvisionedController: DeviceProvisionedController,
+ private val keyguardStateController: KeyguardStateController,
+ private val powerManager: PowerManager,
+ private val statusBarStateController: StatusBarStateController
+) {
+ interface Decision {
+ val shouldFsi: Boolean
+ val wouldFsiWithoutDnd: Boolean
+ val logReason: String
+ }
+
+ private enum class DecisionImpl(
+ override val shouldFsi: Boolean,
+ override val logReason: String,
+ override val wouldFsiWithoutDnd: Boolean = shouldFsi,
+ val supersedesDnd: Boolean = false
+ ) : Decision {
+ NO_FSI_NO_FULL_SCREEN_INTENT(false, "no full-screen intent", supersedesDnd = true),
+ NO_FSI_SHOW_STICKY_HUN(false, "full-screen intents are disabled", supersedesDnd = true),
+ NO_FSI_NOT_IMPORTANT_ENOUGH(false, "not important enough"),
+ NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(false, "suppressive group alert behavior"),
+ NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(false, "suppressive bubble metadata"),
+ NO_FSI_PACKAGE_SUSPENDED(false, "package suspended"),
+ FSI_DEVICE_NOT_INTERACTIVE(true, "device is not interactive"),
+ FSI_DEVICE_DREAMING(true, "device is dreaming"),
+ FSI_KEYGUARD_SHOWING(true, "keyguard is showing"),
+ NO_FSI_EXPECTED_TO_HUN(false, "expected to heads-up instead"),
+ FSI_KEYGUARD_OCCLUDED(true, "keyguard is occluded"),
+ FSI_LOCKED_SHADE(true, "locked shade"),
+ FSI_DEVICE_NOT_PROVISIONED(true, "device not provisioned"),
+ NO_FSI_NO_HUN_OR_KEYGUARD(false, "no HUN or keyguard"),
+ NO_FSI_SUPPRESSED_BY_DND(false, "suppressed by DND", wouldFsiWithoutDnd = false),
+ NO_FSI_SUPPRESSED_ONLY_BY_DND(false, "suppressed only by DND", wouldFsiWithoutDnd = true)
+ }
+
+ fun makeFullScreenIntentDecision(entry: NotificationEntry, couldHeadsUp: Boolean): Decision {
+ val reasonWithoutDnd = makeDecisionWithoutDnd(entry, couldHeadsUp)
+
+ val suppressedWithoutDnd = !reasonWithoutDnd.shouldFsi
+ val suppressedByDnd = entry.shouldSuppressFullScreenIntent()
+
+ val reasonWithDnd =
+ when {
+ reasonWithoutDnd.supersedesDnd -> reasonWithoutDnd
+ suppressedByDnd && !suppressedWithoutDnd -> NO_FSI_SUPPRESSED_ONLY_BY_DND
+ suppressedByDnd -> NO_FSI_SUPPRESSED_BY_DND
+ else -> reasonWithoutDnd
+ }
+
+ return reasonWithDnd
+ }
+
+ private fun makeDecisionWithoutDnd(
+ entry: NotificationEntry,
+ couldHeadsUp: Boolean
+ ): DecisionImpl {
+ val sbn = entry.sbn
+ val notification = sbn.notification!!
+
+ if (notification.fullScreenIntent == null) {
+ return if (entry.isStickyAndNotDemoted) {
+ NO_FSI_SHOW_STICKY_HUN
+ } else {
+ NO_FSI_NO_FULL_SCREEN_INTENT
+ }
+ }
+
+ if (entry.importance < IMPORTANCE_HIGH) {
+ return NO_FSI_NOT_IMPORTANT_ENOUGH
+ }
+
+ if (sbn.isGroup && notification.suppressAlertingDueToGrouping()) {
+ return NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+ }
+
+ val bubbleMetadata = notification.bubbleMetadata
+ if (bubbleMetadata != null && bubbleMetadata.isNotificationSuppressed) {
+ return NO_FSI_SUPPRESSIVE_BUBBLE_METADATA
+ }
+
+ if (entry.ranking.isSuspended) {
+ return NO_FSI_PACKAGE_SUSPENDED
+ }
+
+ if (!powerManager.isInteractive) {
+ return FSI_DEVICE_NOT_INTERACTIVE
+ }
+
+ if (statusBarStateController.isDreaming) {
+ return FSI_DEVICE_DREAMING
+ }
+
+ if (statusBarStateController.state == KEYGUARD) {
+ return FSI_KEYGUARD_SHOWING
+ }
+
+ if (couldHeadsUp) {
+ return NO_FSI_EXPECTED_TO_HUN
+ }
+
+ if (keyguardStateController.isShowing) {
+ return if (keyguardStateController.isOccluded) {
+ FSI_KEYGUARD_OCCLUDED
+ } else {
+ FSI_LOCKED_SHADE
+ }
+ }
+
+ if (!deviceProvisionedController.isDeviceProvisioned) {
+ return FSI_DEVICE_NOT_PROVISIONED
+ }
+
+ return NO_FSI_NO_HUN_OR_KEYGUARD
+ }
+}
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 2730683..9640682 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
@@ -18,7 +18,6 @@
import android.hardware.display.AmbientDisplayConfiguration
import android.os.Handler
import android.os.PowerManager
-import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -30,7 +29,9 @@
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.DeviceProvisionedController
import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
@@ -40,9 +41,11 @@
constructor(
private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
private val batteryController: BatteryController,
+ deviceProvisionedController: DeviceProvisionedController,
private val globalSettings: GlobalSettings,
private val headsUpManager: HeadsUpManager,
private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
+ keyguardStateController: KeyguardStateController,
private val logger: NotificationInterruptLogger,
@Main private val mainHandler: Handler,
private val powerManager: PowerManager,
@@ -50,6 +53,36 @@
private val systemClock: SystemClock,
private val userTracker: UserTracker,
) : VisualInterruptionDecisionProvider {
+ private class DecisionImpl(
+ override val shouldInterrupt: Boolean,
+ override val logReason: String
+ ) : Decision
+
+ private class FullScreenIntentDecisionImpl(
+ private val fsiDecision: FullScreenIntentDecisionProvider.Decision
+ ) : FullScreenIntentDecision {
+ override val shouldInterrupt
+ get() = fsiDecision.shouldFsi
+
+ override val wouldInterruptWithoutDnd
+ get() = fsiDecision.wouldFsiWithoutDnd
+
+ override val logReason
+ get() = fsiDecision.logReason
+ }
+
+ private val fullScreenIntentDecisionProvider =
+ FullScreenIntentDecisionProvider(
+ deviceProvisionedController,
+ keyguardStateController,
+ powerManager,
+ statusBarStateController
+ )
+
+ private val legacySuppressors = mutableSetOf<NotificationInterruptSuppressor>()
+ private val conditions = mutableListOf<VisualInterruptionCondition>()
+ private val filters = mutableListOf<VisualInterruptionFilter>()
+
private var started = false
override fun start() {
@@ -76,24 +109,6 @@
started = true
}
- private class DecisionImpl(
- override val shouldInterrupt: Boolean,
- override val logReason: String
- ) : Decision
-
- private class FullScreenIntentDecisionImpl(
- override val shouldInterrupt: Boolean,
- override val wouldInterruptWithoutDnd: Boolean,
- override val logReason: String,
- val originalEntry: NotificationEntry,
- ) : FullScreenIntentDecision {
- var hasBeenLogged = false
- }
-
- private val legacySuppressors = mutableSetOf<NotificationInterruptSuppressor>()
- private val conditions = mutableListOf<VisualInterruptionCondition>()
- private val filters = mutableListOf<VisualInterruptionFilter>()
-
override fun addLegacySuppressor(suppressor: NotificationInterruptSuppressor) {
legacySuppressors.add(suppressor)
}
@@ -132,32 +147,21 @@
return makeHeadsUpDecision(entry).also { logHeadsUpDecision(entry, it) }
}
+ override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision {
+ check(started)
+ return makeBubbleDecision(entry).also { logBubbleDecision(entry, it) }
+ }
+
override fun makeUnloggedFullScreenIntentDecision(
entry: NotificationEntry
): FullScreenIntentDecision {
check(started)
- return makeFullScreenDecision(entry)
+ return makeFullScreenIntentDecision(entry)
}
override fun logFullScreenIntentDecision(decision: FullScreenIntentDecision) {
check(started)
- val decisionImpl =
- decision as? FullScreenIntentDecisionImpl
- ?: run {
- Log.wtf(TAG, "Wrong subclass of FullScreenIntentDecision: $decision")
- return
- }
- if (decision.hasBeenLogged) {
- Log.wtf(TAG, "Already logged decision: $decision")
- return
- }
- logFullScreenIntentDecision(decisionImpl)
- decision.hasBeenLogged = true
- }
-
- override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision {
- check(started)
- return makeBubbleDecision(entry).also { logBubbleDecision(entry, it) }
+ // Not yet implemented.
}
private fun makeHeadsUpDecision(entry: NotificationEntry): DecisionImpl {
@@ -234,16 +238,6 @@
return DecisionImpl(shouldInterrupt = true, logReason = "not suppressed")
}
- private fun makeFullScreenDecision(entry: NotificationEntry): FullScreenIntentDecisionImpl {
- // Not yet implemented.
- return FullScreenIntentDecisionImpl(
- shouldInterrupt = true,
- wouldInterruptWithoutDnd = true,
- logReason = "FSI logic not yet implemented in VisualInterruptionDecisionProviderImpl",
- originalEntry = entry
- )
- }
-
private fun logHeadsUpDecision(entry: NotificationEntry, decision: DecisionImpl) {
// Not yet implemented.
}
@@ -252,8 +246,11 @@
// Not yet implemented.
}
- private fun logFullScreenIntentDecision(decision: FullScreenIntentDecisionImpl) {
- // Not yet implemented.
+ private fun makeFullScreenIntentDecision(entry: NotificationEntry): FullScreenIntentDecision {
+ val wouldHeadsUp = makeUnloggedHeadsUpDecision(entry).shouldInterrupt
+ val fsiDecision =
+ fullScreenIntentDecisionProvider.makeFullScreenIntentDecision(entry, wouldHeadsUp)
+ return FullScreenIntentDecisionImpl(fsiDecision)
}
private fun checkSuppressors(entry: NotificationEntry) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index 80d941a..722b170 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -32,9 +32,11 @@
VisualInterruptionDecisionProviderImpl(
ambientDisplayConfiguration,
batteryController,
+ deviceProvisionedController,
globalSettings,
headsUpManager,
keyguardNotificationVisibilityProvider,
+ keyguardStateController,
logger,
mainHandler,
powerManager,
@@ -50,6 +52,7 @@
assertPeekNotSuppressed()
assertPulseNotSuppressed()
assertBubbleNotSuppressed()
+ assertFsiNotSuppressed()
}
}
@@ -59,6 +62,7 @@
assertPeekNotSuppressed()
assertPulseNotSuppressed()
assertBubbleNotSuppressed()
+ assertFsiNotSuppressed()
}
}
@@ -68,6 +72,7 @@
assertPeekSuppressed()
assertPulseNotSuppressed()
assertBubbleNotSuppressed()
+ assertFsiNotSuppressed()
}
}
@@ -77,6 +82,7 @@
assertPeekSuppressed()
assertPulseNotSuppressed()
assertBubbleNotSuppressed()
+ assertFsiNotSuppressed()
}
}
@@ -86,6 +92,7 @@
assertPeekNotSuppressed()
assertPulseSuppressed()
assertBubbleNotSuppressed()
+ assertFsiNotSuppressed()
}
}
@@ -95,6 +102,7 @@
assertPeekNotSuppressed()
assertPulseSuppressed()
assertBubbleNotSuppressed()
+ assertFsiNotSuppressed()
}
}
@@ -104,6 +112,7 @@
assertPeekNotSuppressed()
assertPulseNotSuppressed()
assertBubbleSuppressed()
+ assertFsiNotSuppressed()
}
}
@@ -113,6 +122,7 @@
assertPeekNotSuppressed()
assertPulseNotSuppressed()
assertBubbleSuppressed()
+ assertFsiNotSuppressed()
}
}
@@ -193,6 +203,10 @@
assertShouldBubble(buildBubbleEntry())
}
+ private fun assertFsiNotSuppressed() {
+ forEachFsiState { assertShouldFsi(buildFsiEntry()) }
+ }
+
private fun withCondition(condition: VisualInterruptionCondition, block: () -> Unit) {
provider.addCondition(condition)
block()
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 b05d56a..0f29836 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
@@ -20,6 +20,7 @@
import android.app.Notification
import android.app.Notification.BubbleMetadata
import android.app.Notification.FLAG_BUBBLE
+import android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED
import android.app.Notification.GROUP_ALERT_ALL
import android.app.Notification.GROUP_ALERT_CHILDREN
import android.app.Notification.GROUP_ALERT_SUMMARY
@@ -29,6 +30,7 @@
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.NotificationManager.IMPORTANCE_LOW
import android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT
+import android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
import android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK
import android.app.NotificationManager.VISIBILITY_NO_OVERRIDE
import android.app.PendingIntent
@@ -56,18 +58,19 @@
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.FakeDeviceProvisionedController
import com.android.systemui.statusbar.policy.HeadsUpManager
-import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.settings.FakeGlobalSettings
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.utils.leaks.FakeBatteryController
+import com.android.systemui.utils.leaks.FakeKeyguardStateController
import com.android.systemui.utils.leaks.LeakCheckedTest
import com.android.systemui.utils.os.FakeHandler
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
+import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.`when` as whenever
@@ -77,13 +80,13 @@
protected val ambientDisplayConfiguration = FakeAmbientDisplayConfiguration(context)
protected val batteryController = FakeBatteryController(leakCheck)
- protected val deviceProvisionedController: DeviceProvisionedController = mock()
+ protected val deviceProvisionedController = FakeDeviceProvisionedController()
protected val flags: NotifPipelineFlags = mock()
protected val globalSettings = FakeGlobalSettings()
protected val headsUpManager: HeadsUpManager = mock()
protected val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider =
mock()
- protected val keyguardStateController: KeyguardStateController = mock()
+ protected val keyguardStateController = FakeKeyguardStateController(leakCheck)
protected val logger: NotificationInterruptLogger = mock()
protected val mainHandler = FakeHandler(Looper.getMainLooper())
protected val powerManager: PowerManager = mock()
@@ -137,15 +140,18 @@
}
@Test
- fun testShouldNotPeek_packageSnoozed() {
+ fun testShouldNotPeek_packageSnoozed_withoutFsi() {
ensurePeekState { hunSnoozed = true }
assertShouldNotHeadsUp(buildPeekEntry())
}
@Test
- fun testShouldPeek_packageSnoozedButFsi() {
- ensurePeekState { hunSnoozed = true }
- assertShouldHeadsUp(buildFsiEntry())
+ fun testShouldPeek_packageSnoozed_withFsi() {
+ val entry = buildFsiEntry()
+ forEachPeekableFsiState {
+ ensurePeekState { hunSnoozed = true }
+ assertShouldHeadsUp(entry)
+ }
}
@Test
@@ -483,6 +489,107 @@
}
@Test
+ fun testShouldNotFsi_noFullScreenIntent() {
+ forEachFsiState { assertShouldNotFsi(buildFsiEntry { hasFsi = false }) }
+ }
+
+ @Test
+ fun testShouldNotFsi_showStickyHun() {
+ forEachFsiState {
+ assertShouldNotFsi(
+ buildFsiEntry {
+ hasFsi = false
+ isStickyAndNotDemoted = true
+ }
+ )
+ }
+ }
+
+ @Test
+ fun testShouldNotFsi_onlyDnd() {
+ forEachFsiState {
+ assertShouldNotFsi(
+ buildFsiEntry { suppressedVisualEffects = SUPPRESSED_EFFECT_FULL_SCREEN_INTENT },
+ expectWouldInterruptWithoutDnd = true
+ )
+ }
+ }
+
+ @Test
+ fun testShouldNotFsi_notImportantEnough() {
+ forEachFsiState { assertShouldNotFsi(buildFsiEntry { importance = IMPORTANCE_DEFAULT }) }
+ }
+
+ @Test
+ fun testShouldNotFsi_notOnlyDnd() {
+ forEachFsiState {
+ assertShouldNotFsi(
+ buildFsiEntry {
+ suppressedVisualEffects = SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
+ importance = IMPORTANCE_DEFAULT
+ },
+ expectWouldInterruptWithoutDnd = false
+ )
+ }
+ }
+
+ @Test
+ fun testShouldNotFsi_suppressiveGroupAlertBehavior() {
+ forEachFsiState {
+ assertShouldNotFsi(
+ buildFsiEntry {
+ isGrouped = true
+ isGroupSummary = true
+ groupAlertBehavior = GROUP_ALERT_CHILDREN
+ }
+ )
+ }
+ }
+
+ @Test
+ fun testShouldFsi_suppressiveGroupAlertBehavior_notGrouped() {
+ forEachFsiState {
+ assertShouldFsi(
+ buildFsiEntry {
+ isGrouped = false
+ isGroupSummary = true
+ groupAlertBehavior = GROUP_ALERT_CHILDREN
+ }
+ )
+ }
+ }
+
+ @Test
+ fun testShouldFsi_suppressiveGroupAlertBehavior_notSuppressive() {
+ forEachFsiState {
+ assertShouldFsi(
+ buildFsiEntry {
+ isGrouped = true
+ isGroupSummary = true
+ groupAlertBehavior = GROUP_ALERT_ALL
+ }
+ )
+ }
+ }
+
+ @Test
+ fun testShouldNotFsi_suppressiveBubbleMetadata() {
+ forEachFsiState {
+ assertShouldNotFsi(
+ buildFsiEntry {
+ hasBubbleMetadata = true
+ bubbleSuppressesNotification = true
+ }
+ )
+ }
+ }
+
+ @Test
+ fun testShouldNotFsi_packageSuspended() {
+ forEachFsiState { assertShouldNotFsi(buildFsiEntry { packageSuspended = true }) }
+ }
+
+ @Test
fun testShouldFsi_notInteractive() {
ensureNotInteractiveFsiState()
assertShouldFsi(buildFsiEntry())
@@ -500,6 +607,76 @@
assertShouldFsi(buildFsiEntry())
}
+ @Test
+ fun testShouldNotFsi_expectedToHun() {
+ forEachPeekableFsiState {
+ ensurePeekState()
+ assertShouldNotFsi(buildFsiEntry())
+ }
+ }
+
+ @Test
+ fun testShouldNotFsi_expectedToHun_hunSnoozed() {
+ forEachPeekableFsiState {
+ ensurePeekState { hunSnoozed = true }
+ assertShouldNotFsi(buildFsiEntry())
+ }
+ }
+
+ @Test
+ fun testShouldFsi_lockedShade() {
+ ensureLockedShadeFsiState()
+ assertShouldFsi(buildFsiEntry())
+ }
+
+ @Test
+ fun testShouldFsi_keyguardOccluded() {
+ ensureKeyguardOccludedFsiState()
+ assertShouldFsi(buildFsiEntry())
+ }
+
+ @Test
+ fun testShouldFsi_deviceNotProvisioned() {
+ ensureDeviceNotProvisionedFsiState()
+ assertShouldFsi(buildFsiEntry())
+ }
+
+ @Test
+ fun testShouldNotFsi_noHunOrKeyguard() {
+ ensureNoHunOrKeyguardFsiState()
+ assertShouldNotFsi(buildFsiEntry())
+ }
+
+ @Test
+ fun testShouldFsi_defaultLegacySuppressor() {
+ forEachFsiState {
+ withLegacySuppressor(neverSuppresses) { assertShouldFsi(buildFsiEntry()) }
+ }
+ }
+
+ @Test
+ fun testShouldFsi_suppressInterruptions() {
+ forEachFsiState {
+ withLegacySuppressor(alwaysSuppressesInterruptions) { assertShouldFsi(buildFsiEntry()) }
+ }
+ }
+
+ @Test
+ fun testShouldFsi_suppressAwakeInterruptions() {
+ forEachFsiState {
+ withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
+ assertShouldFsi(buildFsiEntry())
+ }
+ }
+ }
+
+ @Test
+ fun testShouldFsi_suppressAwakeHeadsUp() {
+ forEachFsiState {
+ withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) { assertShouldFsi(buildFsiEntry()) }
+ }
+ }
+
protected data class State(
var hunSettingEnabled: Boolean? = null,
var hunSnoozed: Boolean? = null,
@@ -511,6 +688,9 @@
var keyguardShouldHideNotification: Boolean? = null,
var pulseOnNotificationsEnabled: Boolean? = null,
var statusBarState: Int? = null,
+ var keyguardIsShowing: Boolean = false,
+ var keyguardIsOccluded: Boolean = false,
+ var deviceProvisioned: Boolean = true
)
protected fun setState(state: State): Unit =
@@ -542,6 +722,11 @@
}
statusBarState?.let { statusBarStateController.state = it }
+
+ keyguardStateController.isOccluded = keyguardIsOccluded
+ keyguardStateController.isShowing = keyguardIsShowing
+
+ deviceProvisionedController.deviceProvisioned = deviceProvisioned
}
protected fun ensureState(block: State.() -> Unit) =
@@ -571,26 +756,95 @@
protected fun ensureBubbleState(block: State.() -> Unit = {}) = ensureState(block)
protected fun ensureNotInteractiveFsiState(block: State.() -> Unit = {}) = ensureState {
- isDreaming = false
isInteractive = false
- statusBarState = SHADE
run(block)
}
protected fun ensureDreamingFsiState(block: State.() -> Unit = {}) = ensureState {
- isDreaming = true
isInteractive = true
- statusBarState = SHADE
+ isDreaming = true
run(block)
}
protected fun ensureKeyguardFsiState(block: State.() -> Unit = {}) = ensureState {
- isDreaming = false
isInteractive = true
+ isDreaming = false
statusBarState = KEYGUARD
run(block)
}
+ protected fun ensureLockedShadeFsiState(block: State.() -> Unit = {}) = ensureState {
+ // It is assumed *but not checked in the code* that statusBarState is SHADE_LOCKED.
+ isInteractive = true
+ isDreaming = false
+ statusBarState = SHADE
+ hunSettingEnabled = false
+ keyguardIsShowing = true
+ keyguardIsOccluded = false
+ run(block)
+ }
+
+ protected fun ensureKeyguardOccludedFsiState(block: State.() -> Unit = {}) = ensureState {
+ isInteractive = true
+ isDreaming = false
+ statusBarState = SHADE
+ hunSettingEnabled = false
+ keyguardIsShowing = true
+ keyguardIsOccluded = true
+ run(block)
+ }
+
+ protected fun ensureDeviceNotProvisionedFsiState(block: State.() -> Unit = {}) = ensureState {
+ isInteractive = true
+ isDreaming = false
+ statusBarState = SHADE
+ hunSettingEnabled = false
+ keyguardIsShowing = false
+ deviceProvisioned = false
+ run(block)
+ }
+
+ protected fun ensureNoHunOrKeyguardFsiState(block: State.() -> Unit = {}) = ensureState {
+ isInteractive = true
+ isDreaming = false
+ statusBarState = SHADE
+ hunSettingEnabled = false
+ keyguardIsShowing = false
+ deviceProvisioned = true
+ run(block)
+ }
+
+ protected fun forEachFsiState(block: () -> Unit) {
+ ensureNotInteractiveFsiState()
+ block()
+
+ ensureDreamingFsiState()
+ block()
+
+ ensureKeyguardFsiState()
+ block()
+
+ ensureLockedShadeFsiState()
+ block()
+
+ ensureKeyguardOccludedFsiState()
+ block()
+
+ ensureDeviceNotProvisionedFsiState()
+ block()
+ }
+
+ private fun forEachPeekableFsiState(extendState: State.() -> Unit = {}, block: () -> Unit) {
+ ensureLockedShadeFsiState(extendState)
+ block()
+
+ ensureKeyguardOccludedFsiState(extendState)
+ block()
+
+ ensureDeviceNotProvisionedFsiState(extendState)
+ block()
+ }
+
protected fun withLegacySuppressor(
suppressor: NotificationInterruptSuppressor,
block: () -> Unit
@@ -625,9 +879,19 @@
assertTrue("unexpected suppressed FSI: ${it.logReason}", it.shouldInterrupt)
}
- protected fun assertShouldNotFsi(entry: NotificationEntry) =
+ protected fun assertShouldNotFsi(
+ entry: NotificationEntry,
+ expectWouldInterruptWithoutDnd: Boolean? = null
+ ) =
provider.makeUnloggedFullScreenIntentDecision(entry).let {
assertFalse("unexpected unsuppressed FSI: ${it.logReason}", it.shouldInterrupt)
+ if (expectWouldInterruptWithoutDnd != null) {
+ assertEquals(
+ "unexpected unsuppressed-without-DND FSI: ${it.logReason}",
+ expectWouldInterruptWithoutDnd,
+ it.wouldInterruptWithoutDnd
+ )
+ }
}
protected class EntryBuilder(val context: Context) {
@@ -645,6 +909,8 @@
var isGroupSummary: Boolean? = null
var groupAlertBehavior: Int? = null
var hasJustLaunchedFsi = false
+ var isStickyAndNotDemoted = false
+ var packageSuspended: Boolean? = null
private fun buildBubbleMetadata(): BubbleMetadata {
val builder =
@@ -696,6 +962,10 @@
if (isBubble) {
flags = flags or FLAG_BUBBLE
}
+
+ if (isStickyAndNotDemoted) {
+ flags = flags or FLAG_FSI_REQUESTED_BUT_DENIED
+ }
}
.let { NotificationEntryBuilder().setNotification(it) }
.apply {
@@ -714,10 +984,15 @@
it.notifyFullScreenIntentLaunched()
}
+ if (isStickyAndNotDemoted) {
+ assertFalse(it.isDemoted)
+ }
+
modifyRanking(it)
.apply {
suppressedVisualEffects?.let { setSuppressedVisualEffects(it) }
visibilityOverride?.let { setVisibilityOverride(it) }
+ packageSuspended?.let { setSuspended(it) }
}
.build()
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt
new file mode 100644
index 0000000..0c2b115
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt
@@ -0,0 +1,31 @@
+package com.android.systemui.statusbar.policy
+
+class FakeDeviceProvisionedController : DeviceProvisionedController {
+ @JvmField var deviceProvisioned = true
+
+ override fun addCallback(listener: DeviceProvisionedController.DeviceProvisionedListener) {
+ TODO("Not yet implemented")
+ }
+
+ override fun removeCallback(listener: DeviceProvisionedController.DeviceProvisionedListener) {
+ TODO("Not yet implemented")
+ }
+
+ override fun isDeviceProvisioned() = deviceProvisioned
+
+ override fun getCurrentUser(): Int {
+ TODO("Not yet implemented")
+ }
+
+ override fun isUserSetup(user: Int): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun isCurrentUserSetup(): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun isFrpActive(): Boolean {
+ TODO("Not yet implemented")
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
index bdf1aff..d452810 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
@@ -24,6 +24,9 @@
private final BaseLeakChecker<Callback> mCallbackController;
+ private boolean mIsShowing = false;
+ private boolean mIsOccluded = false;
+
public FakeKeyguardStateController(LeakCheck test) {
mCallbackController = new BaseLeakChecker<Callback>(test, "keyguard");
}
@@ -45,7 +48,11 @@
@Override
public boolean isShowing() {
- return false;
+ return mIsShowing;
+ }
+
+ public void setShowing(boolean showing) {
+ mIsShowing = showing;
}
@Override
@@ -60,7 +67,11 @@
@Override
public boolean isOccluded() {
- return false;
+ return mIsOccluded;
+ }
+
+ public void setOccluded(boolean occluded) {
+ mIsOccluded = occluded;
}
@Override