Merge changes Iff50fbf8,Ibfd590be,Ibc4484bb into main

* changes:
  Add logging to LogBuffer
  Refactor VisualInterruptionDecisionProviderImpl
  Improve tests and add missing test cases
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 538be14..9ff416a 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
@@ -43,9 +43,9 @@
 class PeekDisabledSuppressor(
     private val globalSettings: GlobalSettings,
     private val headsUpManager: HeadsUpManager,
-    private val logger: NotificationInterruptLogger,
+    private val logger: VisualInterruptionDecisionLogger,
     @Main private val mainHandler: Handler,
-) : VisualInterruptionCondition(types = setOf(PEEK), reason = "peek setting disabled") {
+) : VisualInterruptionCondition(types = setOf(PEEK), reason = "peek disabled by global setting") {
     private var isEnabled = false
 
     override fun shouldSuppress(): Boolean = !isEnabled
@@ -87,16 +87,13 @@
 class PulseDisabledSuppressor(
     private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
     private val userTracker: UserTracker,
-) : VisualInterruptionCondition(types = setOf(PULSE), reason = "pulse setting disabled") {
+) : VisualInterruptionCondition(types = setOf(PULSE), reason = "pulse disabled by user setting") {
     override fun shouldSuppress(): Boolean =
         !ambientDisplayConfiguration.pulseOnNotificationEnabled(userTracker.userId)
 }
 
 class PulseBatterySaverSuppressor(private val batteryController: BatteryController) :
-    VisualInterruptionCondition(
-        types = setOf(PULSE),
-        reason = "pulsing disabled by battery saver"
-    ) {
+    VisualInterruptionCondition(types = setOf(PULSE), reason = "pulse disabled by battery saver") {
     override fun shouldSuppress() = batteryController.isAodPowerSave()
 }
 
@@ -128,14 +125,14 @@
 }
 
 class PeekNotImportantSuppressor() :
-    VisualInterruptionFilter(types = setOf(PEEK), reason = "not important") {
+    VisualInterruptionFilter(types = setOf(PEEK), reason = "importance < HIGH") {
     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") {
+) : VisualInterruptionCondition(types = setOf(PEEK), reason = "device not in use") {
     override fun shouldSuppress() =
         when {
             !powerManager.isScreenOn || statusBarStateController.isDreaming -> true
@@ -144,7 +141,7 @@
 }
 
 class PeekOldWhenSuppressor(private val systemClock: SystemClock) :
-    VisualInterruptionFilter(types = setOf(PEEK), reason = "old when") {
+    VisualInterruptionFilter(types = setOf(PEEK), reason = "has old `when`") {
     private fun whenAge(entry: NotificationEntry) =
         systemClock.currentTimeMillis() - entry.sbn.notification.`when`
 
@@ -165,21 +162,21 @@
 }
 
 class PulseEffectSuppressor() :
-    VisualInterruptionFilter(types = setOf(PULSE), reason = "ambient effect suppressed") {
+    VisualInterruptionFilter(types = setOf(PULSE), reason = "suppressed by DND") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressAmbient()
 }
 
 class PulseLockscreenVisibilityPrivateSuppressor() :
     VisualInterruptionFilter(
         types = setOf(PULSE),
-        reason = "notification hidden on lock screen by override"
+        reason = "hidden by lockscreen visibility override"
     ) {
     override fun shouldSuppress(entry: NotificationEntry) =
         entry.ranking.lockscreenVisibilityOverride == VISIBILITY_PRIVATE
 }
 
 class PulseLowImportanceSuppressor() :
-    VisualInterruptionFilter(types = setOf(PULSE), reason = "importance less than DEFAULT") {
+    VisualInterruptionFilter(types = setOf(PULSE), reason = "importance < DEFAULT") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_DEFAULT
 }
 
@@ -198,12 +195,12 @@
 }
 
 class BubbleNotAllowedSuppressor() :
-    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "not allowed") {
+    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble") {
     override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble()
 }
 
 class BubbleNoMetadataSuppressor() :
-    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "no bubble metadata") {
+    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") {
 
     private fun isValidMetadata(metadata: BubbleMetadata?) =
         metadata != null && (metadata.intent != null || metadata.shortcutId != null)
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
index 6af2543..b44a367 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
@@ -50,19 +50,32 @@
         val shouldFsi: Boolean
         val wouldFsiWithoutDnd: Boolean
         val logReason: String
+        val shouldLog: Boolean
+        val isWarning: Boolean
     }
 
     private enum class DecisionImpl(
         override val shouldFsi: Boolean,
         override val logReason: String,
         override val wouldFsiWithoutDnd: Boolean = shouldFsi,
-        val supersedesDnd: Boolean = false
+        val supersedesDnd: Boolean = false,
+        override val shouldLog: Boolean = true,
+        override val isWarning: Boolean = false
     ) : Decision {
-        NO_FSI_NO_FULL_SCREEN_INTENT(false, "no full-screen intent", supersedesDnd = true),
+        NO_FSI_NO_FULL_SCREEN_INTENT(
+            false,
+            "no full-screen intent",
+            supersedesDnd = true,
+            shouldLog = false
+        ),
         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_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(
+            false,
+            "suppressive group alert behavior",
+            isWarning = true
+        ),
+        NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(false, "suppressive bubble metadata", isWarning = true),
         NO_FSI_PACKAGE_SUSPENDED(false, "package suspended"),
         FSI_DEVICE_NOT_INTERACTIVE(true, "device is not interactive"),
         FSI_DEVICE_DREAMING(true, "device is dreaming"),
@@ -71,7 +84,7 @@
         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_NO_HUN_OR_KEYGUARD(false, "no HUN or keyguard", isWarning = true),
         NO_FSI_SUPPRESSED_BY_DND(false, "suppressed by DND", wouldFsiWithoutDnd = false),
         NO_FSI_SUPPRESSED_ONLY_BY_DND(false, "suppressed only by DND", wouldFsiWithoutDnd = true)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
new file mode 100644
index 0000000..1470b03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
@@ -0,0 +1,93 @@
+/*
+ * 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 com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.WARNING
+import com.android.systemui.log.dagger.NotificationInterruptLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision
+import com.android.systemui.statusbar.notification.logKey
+import javax.inject.Inject
+
+class VisualInterruptionDecisionLogger
+@Inject
+constructor(@NotificationInterruptLog val buffer: LogBuffer) {
+    fun logHeadsUpFeatureChanged(isEnabled: Boolean) {
+        buffer.log(
+            TAG,
+            INFO,
+            { bool1 = isEnabled },
+            { "HUN feature is now ${if (bool1) "enabled" else "disabled"}" }
+        )
+    }
+
+    fun logWillDismissAll() {
+        buffer.log(TAG, INFO, {}, { "dismissing all HUNs since feature was disabled" })
+    }
+
+    fun logDecision(
+        type: String,
+        entry: NotificationEntry,
+        decision: VisualInterruptionDecisionProvider.Decision
+    ) {
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = type
+                bool1 = decision.shouldInterrupt
+                str2 = decision.logReason
+                str3 = entry.logKey
+            },
+            {
+                val outcome = if (bool1) "allowed" else "suppressed"
+                "$str1 $outcome: $str2 (key=$str3)"
+            }
+        )
+    }
+
+    fun logFullScreenIntentDecision(
+        entry: NotificationEntry,
+        decision: FullScreenIntentDecision,
+        warning: Boolean
+    ) {
+        buffer.log(
+            TAG,
+            if (warning) WARNING else DEBUG,
+            {
+                bool1 = decision.shouldInterrupt
+                bool2 = decision.wouldInterruptWithoutDnd
+                str1 = decision.logReason
+                str2 = entry.logKey
+            },
+            {
+                val outcome =
+                    when {
+                        bool1 -> "allowed"
+                        bool2 -> "suppressed only by DND"
+                        else -> "suppressed"
+                    }
+                "FSI $outcome: $str1 (key=$str2)"
+            }
+        )
+    }
+}
+
+private const val TAG = "VisualInterruptionDecisionProvider"
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 9640682..c0a1a32 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,6 +18,7 @@
 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
@@ -46,7 +47,7 @@
     private val headsUpManager: HeadsUpManager,
     private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
     keyguardStateController: KeyguardStateController,
-    private val logger: NotificationInterruptLogger,
+    private val logger: VisualInterruptionDecisionLogger,
     @Main private val mainHandler: Handler,
     private val powerManager: PowerManager,
     private val statusBarStateController: StatusBarStateController,
@@ -58,9 +59,32 @@
         override val logReason: String
     ) : Decision
 
+    private data class LoggableDecision private constructor(val decision: DecisionImpl) {
+        companion object {
+            val unsuppressed =
+                LoggableDecision(DecisionImpl(shouldInterrupt = true, logReason = "not suppressed"))
+
+            fun suppressed(legacySuppressor: NotificationInterruptSuppressor, methodName: String) =
+                LoggableDecision(
+                    DecisionImpl(
+                        shouldInterrupt = false,
+                        logReason = "${legacySuppressor.name}.$methodName"
+                    )
+                )
+
+            fun suppressed(suppressor: VisualInterruptionSuppressor) =
+                LoggableDecision(
+                    DecisionImpl(shouldInterrupt = false, logReason = suppressor.reason)
+                )
+        }
+    }
+
     private class FullScreenIntentDecisionImpl(
+        val entry: NotificationEntry,
         private val fsiDecision: FullScreenIntentDecisionProvider.Decision
     ) : FullScreenIntentDecision {
+        var hasBeenLogged = false
+
         override val shouldInterrupt
             get() = fsiDecision.shouldFsi
 
@@ -69,6 +93,12 @@
 
         override val logReason
             get() = fsiDecision.logReason
+
+        val shouldLog
+            get() = fsiDecision.shouldLog
+
+        val isWarning
+            get() = fsiDecision.isWarning
     }
 
     private val fullScreenIntentDecisionProvider =
@@ -139,137 +169,113 @@
 
     override fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision {
         check(started)
-        return makeHeadsUpDecision(entry)
+
+        return if (statusBarStateController.isDozing) {
+                makeLoggablePulseDecision(entry)
+            } else {
+                makeLoggablePeekDecision(entry)
+            }
+            .decision
     }
 
     override fun makeAndLogHeadsUpDecision(entry: NotificationEntry): Decision {
         check(started)
-        return makeHeadsUpDecision(entry).also { logHeadsUpDecision(entry, it) }
+
+        return if (statusBarStateController.isDozing) {
+                makeLoggablePulseDecision(entry).also { logDecision(PULSE, entry, it) }
+            } else {
+                makeLoggablePeekDecision(entry).also { logDecision(PEEK, entry, it) }
+            }
+            .decision
     }
 
+    private fun makeLoggablePeekDecision(entry: NotificationEntry): LoggableDecision =
+        checkConditions(PEEK)
+            ?: checkFilters(PEEK, entry) ?: checkSuppressInterruptions(entry)
+                ?: checkSuppressAwakeInterruptions(entry) ?: checkSuppressAwakeHeadsUp(entry)
+                ?: LoggableDecision.unsuppressed
+
+    private fun makeLoggablePulseDecision(entry: NotificationEntry): LoggableDecision =
+        checkConditions(PULSE)
+            ?: checkFilters(PULSE, entry) ?: checkSuppressInterruptions(entry)
+                ?: LoggableDecision.unsuppressed
+
     override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision {
         check(started)
-        return makeBubbleDecision(entry).also { logBubbleDecision(entry, it) }
+
+        return makeLoggableBubbleDecision(entry).also { logDecision(BUBBLE, entry, it) }.decision
+    }
+
+    private fun makeLoggableBubbleDecision(entry: NotificationEntry): LoggableDecision =
+        checkConditions(BUBBLE)
+            ?: checkFilters(BUBBLE, entry) ?: checkSuppressInterruptions(entry)
+                ?: checkSuppressAwakeInterruptions(entry) ?: LoggableDecision.unsuppressed
+
+    private fun logDecision(
+        type: VisualInterruptionType,
+        entry: NotificationEntry,
+        loggable: LoggableDecision
+    ) {
+        logger.logDecision(type.name, entry, loggable.decision)
     }
 
     override fun makeUnloggedFullScreenIntentDecision(
         entry: NotificationEntry
     ): FullScreenIntentDecision {
         check(started)
-        return makeFullScreenIntentDecision(entry)
+
+        val couldHeadsUp = makeUnloggedHeadsUpDecision(entry).shouldInterrupt
+        val fsiDecision =
+            fullScreenIntentDecisionProvider.makeFullScreenIntentDecision(entry, couldHeadsUp)
+        return FullScreenIntentDecisionImpl(entry, fsiDecision)
     }
 
     override fun logFullScreenIntentDecision(decision: FullScreenIntentDecision) {
         check(started)
-        // Not yet implemented.
+
+        if (decision !is FullScreenIntentDecisionImpl) {
+            Log.wtf(TAG, "FSI decision $decision was not created by this class")
+            return
+        }
+
+        if (decision.hasBeenLogged) {
+            Log.wtf(TAG, "FSI decision $decision has already been logged")
+            return
+        }
+
+        decision.hasBeenLogged = true
+
+        if (!decision.shouldLog) {
+            return
+        }
+
+        logger.logFullScreenIntentDecision(decision.entry, decision, decision.isWarning)
     }
 
-    private fun makeHeadsUpDecision(entry: NotificationEntry): DecisionImpl {
-        if (statusBarStateController.isDozing) {
-            return makePulseDecision(entry)
-        } else {
-            return makePeekDecision(entry)
-        }
-    }
+    private fun checkSuppressInterruptions(entry: NotificationEntry) =
+        legacySuppressors
+            .firstOrNull { it.suppressInterruptions(entry) }
+            ?.let { LoggableDecision.suppressed(it, "suppressInterruptions") }
 
-    private fun makePeekDecision(entry: NotificationEntry): DecisionImpl {
-        checkConditions(PEEK)?.let {
-            return DecisionImpl(shouldInterrupt = false, logReason = it.reason)
-        }
-        checkFilters(PEEK, entry)?.let {
-            return DecisionImpl(shouldInterrupt = false, logReason = it.reason)
-        }
-        checkSuppressors(entry)?.let {
-            return DecisionImpl(
-                shouldInterrupt = false,
-                logReason = "${it.name}.suppressInterruptions"
-            )
-        }
-        checkAwakeSuppressors(entry)?.let {
-            return DecisionImpl(
-                shouldInterrupt = false,
-                logReason = "${it.name}.suppressAwakeInterruptions"
-            )
-        }
-        checkAwakeHeadsUpSuppressors(entry)?.let {
-            return DecisionImpl(
-                shouldInterrupt = false,
-                logReason = "${it.name}.suppressAwakeHeadsUpInterruptions"
-            )
-        }
-        return DecisionImpl(shouldInterrupt = true, logReason = "not suppressed")
-    }
+    private fun checkSuppressAwakeInterruptions(entry: NotificationEntry) =
+        legacySuppressors
+            .firstOrNull { it.suppressAwakeInterruptions(entry) }
+            ?.let { LoggableDecision.suppressed(it, "suppressAwakeInterruptions") }
 
-    private fun makePulseDecision(entry: NotificationEntry): DecisionImpl {
-        checkConditions(PULSE)?.let {
-            return DecisionImpl(shouldInterrupt = false, logReason = it.reason)
-        }
-        checkFilters(PULSE, entry)?.let {
-            return DecisionImpl(shouldInterrupt = false, logReason = it.reason)
-        }
-        checkSuppressors(entry)?.let {
-            return DecisionImpl(
-                shouldInterrupt = false,
-                logReason = "${it.name}.suppressInterruptions"
-            )
-        }
-        return DecisionImpl(shouldInterrupt = true, logReason = "not suppressed")
-    }
+    private fun checkSuppressAwakeHeadsUp(entry: NotificationEntry) =
+        legacySuppressors
+            .firstOrNull { it.suppressAwakeHeadsUp(entry) }
+            ?.let { LoggableDecision.suppressed(it, "suppressAwakeHeadsUp") }
 
-    private fun makeBubbleDecision(entry: NotificationEntry): DecisionImpl {
-        checkConditions(BUBBLE)?.let {
-            return DecisionImpl(shouldInterrupt = false, logReason = it.reason)
-        }
-        checkFilters(BUBBLE, entry)?.let {
-            return DecisionImpl(shouldInterrupt = false, logReason = it.reason)
-        }
-        checkSuppressors(entry)?.let {
-            return DecisionImpl(
-                shouldInterrupt = false,
-                logReason = "${it.name}.suppressInterruptions"
-            )
-        }
-        checkAwakeSuppressors(entry)?.let {
-            return DecisionImpl(
-                shouldInterrupt = false,
-                logReason = "${it.name}.suppressAwakeInterruptions"
-            )
-        }
-        return DecisionImpl(shouldInterrupt = true, logReason = "not suppressed")
-    }
+    private fun checkConditions(type: VisualInterruptionType) =
+        conditions
+            .firstOrNull { it.types.contains(type) && it.shouldSuppress() }
+            ?.let { LoggableDecision.suppressed(it) }
 
-    private fun logHeadsUpDecision(entry: NotificationEntry, decision: DecisionImpl) {
-        // Not yet implemented.
-    }
-
-    private fun logBubbleDecision(entry: NotificationEntry, decision: DecisionImpl) {
-        // 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) =
-        legacySuppressors.firstOrNull { it.suppressInterruptions(entry) }
-
-    private fun checkAwakeSuppressors(entry: NotificationEntry) =
-        legacySuppressors.firstOrNull { it.suppressAwakeInterruptions(entry) }
-
-    private fun checkAwakeHeadsUpSuppressors(entry: NotificationEntry) =
-        legacySuppressors.firstOrNull { it.suppressAwakeHeadsUp(entry) }
-
-    private fun checkConditions(type: VisualInterruptionType): VisualInterruptionCondition? =
-        conditions.firstOrNull { it.types.contains(type) && it.shouldSuppress() }
-
-    private fun checkFilters(
-        type: VisualInterruptionType,
-        entry: NotificationEntry
-    ): VisualInterruptionFilter? =
-        filters.firstOrNull { it.types.contains(type) && it.shouldSuppress(entry) }
+    private fun checkFilters(type: VisualInterruptionType, entry: NotificationEntry) =
+        filters
+            .firstOrNull { it.types.contains(type) && it.shouldSuppress(entry) }
+            ?.let { LoggableDecision.suppressed(it) }
 }
 
 private const val TAG = "VisualInterruptionDecisionProviderImpl"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
index 1d2055e..e1581ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
@@ -44,7 +44,7 @@
                 statusBarStateController,
                 keyguardStateController,
                 headsUpManager,
-                logger,
+                oldLogger,
                 mainHandler,
                 flags,
                 keyguardNotificationVisibilityProvider,
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 722b170..1064475 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
@@ -37,7 +37,7 @@
             headsUpManager,
             keyguardNotificationVisibilityProvider,
             keyguardStateController,
-            logger,
+            newLogger,
             mainHandler,
             powerManager,
             statusBarStateController,
@@ -222,14 +222,14 @@
     private class TestCondition(
         types: Set<VisualInterruptionType>,
         val onShouldSuppress: () -> Boolean
-    ) : VisualInterruptionCondition(types = types, reason = "") {
+    ) : VisualInterruptionCondition(types = types, reason = "test condition") {
         override fun shouldSuppress(): Boolean = onShouldSuppress()
     }
 
     private class TestFilter(
         types: Set<VisualInterruptionType>,
         val onShouldSuppress: (NotificationEntry) -> Boolean = { true }
-    ) : VisualInterruptionFilter(types = types, reason = "") {
+    ) : VisualInterruptionFilter(types = types, reason = "test filter") {
         override fun shouldSuppress(entry: NotificationEntry) = onShouldSuppress(entry)
     }
 }
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 0f29836..5e81156 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,7 +20,9 @@
 import android.app.Notification
 import android.app.Notification.BubbleMetadata
 import android.app.Notification.FLAG_BUBBLE
+import android.app.Notification.FLAG_FOREGROUND_SERVICE
 import android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED
+import android.app.Notification.FLAG_USER_INITIATED_JOB
 import android.app.Notification.GROUP_ALERT_ALL
 import android.app.Notification.GROUP_ALERT_CHILDREN
 import android.app.Notification.GROUP_ALERT_SUMMARY
@@ -47,6 +49,9 @@
 import android.provider.Settings.Global.HEADS_UP_ON
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.res.R
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.statusbar.FakeStatusBarStateController
@@ -76,19 +81,35 @@
 import org.mockito.Mockito.`when` as whenever
 
 abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() {
+    private val fakeLogBuffer =
+        LogBuffer(
+            name = "FakeLog",
+            maxSize = 1,
+            logcatEchoTracker =
+                object : LogcatEchoTracker {
+                    override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean =
+                        true
+
+                    override fun isTagLoggable(tagName: String, level: LogLevel): Boolean = true
+                },
+            systrace = false
+        )
+
     private val leakCheck = LeakCheckedTest.SysuiLeakCheck()
 
     protected val ambientDisplayConfiguration = FakeAmbientDisplayConfiguration(context)
     protected val batteryController = FakeBatteryController(leakCheck)
     protected val deviceProvisionedController = FakeDeviceProvisionedController()
     protected val flags: NotifPipelineFlags = mock()
-    protected val globalSettings = FakeGlobalSettings()
+    protected val globalSettings =
+        FakeGlobalSettings().also { it.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON) }
     protected val headsUpManager: HeadsUpManager = mock()
     protected val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider =
         mock()
     protected val keyguardStateController = FakeKeyguardStateController(leakCheck)
-    protected val logger: NotificationInterruptLogger = mock()
     protected val mainHandler = FakeHandler(Looper.getMainLooper())
+    protected val newLogger = VisualInterruptionDecisionLogger(fakeLogBuffer)
+    protected val oldLogger = NotificationInterruptLogger(fakeLogBuffer)
     protected val powerManager: PowerManager = mock()
     protected val statusBarStateController = FakeStatusBarStateController()
     protected val systemClock = FakeSystemClock()
@@ -116,14 +137,9 @@
 
     @Before
     fun setUp() {
-        globalSettings.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON)
-
         val user = UserInfo(ActivityManager.getCurrentUser(), "Current user", /* flags = */ 0)
         userTracker.set(listOf(user), /* currentUserIndex = */ 0)
 
-        whenever(keyguardNotificationVisibilityProvider.shouldHideNotification(any()))
-            .thenReturn(false)
-
         provider.start()
     }
 
@@ -203,24 +219,64 @@
     }
 
     @Test
-    fun testShouldPeek_notQuiteOldEnoughWhen() {
+    fun testShouldPeek_oldWhen_now() {
+        ensurePeekState()
+        assertShouldHeadsUp(buildPeekEntry { whenMs = whenAgo(0) })
+    }
+
+    @Test
+    fun testShouldPeek_oldWhen_notOldEnough() {
         ensurePeekState()
         assertShouldHeadsUp(buildPeekEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS - 1) })
     }
 
     @Test
-    fun testShouldPeek_zeroWhen() {
+    fun testShouldPeek_oldWhen_zeroWhen() {
         ensurePeekState()
         assertShouldHeadsUp(buildPeekEntry { whenMs = 0L })
     }
 
     @Test
-    fun testShouldPeek_oldWhenButFsi() {
+    fun testShouldPeek_oldWhen_negativeWhen() {
+        ensurePeekState()
+        assertShouldHeadsUp(buildPeekEntry { whenMs = -1L })
+    }
+
+    @Test
+    fun testShouldPeek_oldWhen_fullScreenIntent() {
         ensurePeekState()
         assertShouldHeadsUp(buildFsiEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS) })
     }
 
     @Test
+    fun testShouldPeek_oldWhen_foregroundService() {
+        ensurePeekState()
+        assertShouldHeadsUp(
+            buildPeekEntry {
+                whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS)
+                isForegroundService = true
+            }
+        )
+    }
+
+    @Test
+    fun testShouldPeek_oldWhen_userInitiatedJob() {
+        ensurePeekState()
+        assertShouldHeadsUp(
+            buildPeekEntry {
+                whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS)
+                isUserInitiatedJob = true
+            }
+        )
+    }
+
+    @Test
+    fun testShouldNotPeek_hiddenOnKeyguard() {
+        ensurePeekState({ keyguardShouldHideNotification = true })
+        assertShouldNotHeadsUp(buildPeekEntry())
+    }
+
+    @Test
     fun testShouldPeek_defaultLegacySuppressor() {
         ensurePeekState()
         withLegacySuppressor(neverSuppresses) { assertShouldHeadsUp(buildPeekEntry()) }
@@ -257,36 +313,6 @@
     }
 
     @Test
-    fun testShouldPulse_defaultLegacySuppressor() {
-        ensurePulseState()
-        withLegacySuppressor(neverSuppresses) { assertShouldHeadsUp(buildPulseEntry()) }
-    }
-
-    @Test
-    fun testShouldNotPulse_legacySuppressInterruptions() {
-        ensurePulseState()
-        withLegacySuppressor(alwaysSuppressesInterruptions) {
-            assertShouldNotHeadsUp(buildPulseEntry())
-        }
-    }
-
-    @Test
-    fun testShouldPulse_legacySuppressAwakeInterruptions() {
-        ensurePulseState()
-        withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
-            assertShouldHeadsUp(buildPulseEntry())
-        }
-    }
-
-    @Test
-    fun testShouldPulse_legacySuppressAwakeHeadsUp() {
-        ensurePulseState()
-        withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
-            assertShouldHeadsUp(buildPulseEntry())
-        }
-    }
-
-    @Test
     fun testShouldNotPulse_disabled() {
         ensurePulseState { pulseOnNotificationsEnabled = false }
         assertShouldNotHeadsUp(buildPulseEntry())
@@ -318,6 +344,42 @@
         assertShouldNotHeadsUp(buildPulseEntry { importance = IMPORTANCE_LOW })
     }
 
+    @Test
+    fun testShouldNotPulse_hiddenOnKeyguard() {
+        ensurePulseState({ keyguardShouldHideNotification = true })
+        assertShouldNotHeadsUp(buildPulseEntry())
+    }
+
+    @Test
+    fun testShouldPulse_defaultLegacySuppressor() {
+        ensurePulseState()
+        withLegacySuppressor(neverSuppresses) { assertShouldHeadsUp(buildPulseEntry()) }
+    }
+
+    @Test
+    fun testShouldNotPulse_legacySuppressInterruptions() {
+        ensurePulseState()
+        withLegacySuppressor(alwaysSuppressesInterruptions) {
+            assertShouldNotHeadsUp(buildPulseEntry())
+        }
+    }
+
+    @Test
+    fun testShouldPulse_legacySuppressAwakeInterruptions() {
+        ensurePulseState()
+        withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
+            assertShouldHeadsUp(buildPulseEntry())
+        }
+    }
+
+    @Test
+    fun testShouldPulse_legacySuppressAwakeHeadsUp() {
+        ensurePulseState()
+        withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
+            assertShouldHeadsUp(buildPulseEntry())
+        }
+    }
+
     private fun withPeekAndPulseEntry(
         extendEntry: EntryBuilder.() -> Unit,
         block: (NotificationEntry) -> Unit
@@ -330,73 +392,7 @@
     }
 
     @Test
-    fun testShouldHeadsUp_groupedSummaryNotif_groupAlertAll() {
-        withPeekAndPulseEntry({
-            isGrouped = true
-            isGroupSummary = true
-            groupAlertBehavior = GROUP_ALERT_ALL
-        }) {
-            assertShouldHeadsUp(it)
-        }
-    }
-
-    @Test
-    fun testShouldHeadsUp_groupedSummaryNotif_groupAlertSummary() {
-        withPeekAndPulseEntry({
-            isGrouped = true
-            isGroupSummary = true
-            groupAlertBehavior = GROUP_ALERT_SUMMARY
-        }) {
-            assertShouldHeadsUp(it)
-        }
-    }
-
-    @Test
-    fun testShouldNotHeadsUp_groupedSummaryNotif_groupAlertChildren() {
-        withPeekAndPulseEntry({
-            isGrouped = true
-            isGroupSummary = true
-            groupAlertBehavior = GROUP_ALERT_CHILDREN
-        }) {
-            assertShouldNotHeadsUp(it)
-        }
-    }
-
-    @Test
-    fun testShouldHeadsUp_ungroupedSummaryNotif_groupAlertChildren() {
-        withPeekAndPulseEntry({
-            isGrouped = false
-            isGroupSummary = true
-            groupAlertBehavior = GROUP_ALERT_CHILDREN
-        }) {
-            assertShouldHeadsUp(it)
-        }
-    }
-
-    @Test
-    fun testShouldHeadsUp_groupedChildNotif_groupAlertAll() {
-        withPeekAndPulseEntry({
-            isGrouped = true
-            isGroupSummary = false
-            groupAlertBehavior = GROUP_ALERT_ALL
-        }) {
-            assertShouldHeadsUp(it)
-        }
-    }
-
-    @Test
-    fun testShouldHeadsUp_groupedChildNotif_groupAlertChildren() {
-        withPeekAndPulseEntry({
-            isGrouped = true
-            isGroupSummary = false
-            groupAlertBehavior = GROUP_ALERT_CHILDREN
-        }) {
-            assertShouldHeadsUp(it)
-        }
-    }
-
-    @Test
-    fun testShouldNotHeadsUp_groupedChildNotif_groupAlertSummary() {
+    fun testShouldNotHeadsUp_suppressiveGroupAlertBehavior() {
         withPeekAndPulseEntry({
             isGrouped = true
             isGroupSummary = false
@@ -407,7 +403,18 @@
     }
 
     @Test
-    fun testShouldHeadsUp_ungroupedChildNotif_groupAlertSummary() {
+    fun testShouldHeadsUp_suppressiveGroupAlertBehavior_notSuppressive() {
+        withPeekAndPulseEntry({
+            isGrouped = true
+            isGroupSummary = false
+            groupAlertBehavior = GROUP_ALERT_CHILDREN
+        }) {
+            assertShouldHeadsUp(it)
+        }
+    }
+
+    @Test
+    fun testShouldHeadsUp_suppressiveGroupAlertBehavior_notGrouped() {
         withPeekAndPulseEntry({
             isGrouped = false
             isGroupSummary = false
@@ -435,18 +442,41 @@
     }
 
     @Test
-    fun testShouldNotBubble_notAllowed() {
+    fun testShouldBubble_suppressiveGroupAlertBehavior() {
         ensureBubbleState()
-        assertShouldNotBubble(buildBubbleEntry { canBubble = false })
+        assertShouldBubble(
+            buildBubbleEntry {
+                isGrouped = true
+                isGroupSummary = false
+                groupAlertBehavior = GROUP_ALERT_SUMMARY
+            }
+        )
     }
 
     @Test
-    fun testShouldNotBubble_noBubbleMetadata() {
+    fun testShouldNotBubble_notABubble() {
+        ensureBubbleState()
+        assertShouldNotBubble(
+            buildBubbleEntry {
+                isBubble = false
+                hasBubbleMetadata = false
+            }
+        )
+    }
+
+    @Test
+    fun testShouldNotBubble_missingBubbleMetadata() {
         ensureBubbleState()
         assertShouldNotBubble(buildBubbleEntry { hasBubbleMetadata = false })
     }
 
     @Test
+    fun testShouldNotBubble_notAllowedToBubble() {
+        ensureBubbleState()
+        assertShouldNotBubble(buildBubbleEntry { canBubble = false })
+    }
+
+    @Test
     fun testShouldBubble_defaultLegacySuppressor() {
         ensureBubbleState()
         withLegacySuppressor(neverSuppresses) { assertShouldBubble(buildBubbleEntry()) }
@@ -477,13 +507,7 @@
     }
 
     @Test
-    fun testShouldNotAlert_hiddenOnKeyguard() {
-        ensurePeekState({ keyguardShouldHideNotification = true })
-        assertShouldNotHeadsUp(buildPeekEntry())
-
-        ensurePulseState({ keyguardShouldHideNotification = true })
-        assertShouldNotHeadsUp(buildPulseEntry())
-
+    fun testShouldNotBubble_hiddenOnKeyguard() {
         ensureBubbleState({ keyguardShouldHideNotification = true })
         assertShouldNotBubble(buildBubbleEntry())
     }
@@ -855,12 +879,12 @@
     }
 
     protected fun assertShouldHeadsUp(entry: NotificationEntry) =
-        provider.makeUnloggedHeadsUpDecision(entry).let {
+        provider.makeAndLogHeadsUpDecision(entry).let {
             assertTrue("unexpected suppressed HUN: ${it.logReason}", it.shouldInterrupt)
         }
 
     protected fun assertShouldNotHeadsUp(entry: NotificationEntry) =
-        provider.makeUnloggedHeadsUpDecision(entry).let {
+        provider.makeAndLogHeadsUpDecision(entry).let {
             assertFalse("unexpected unsuppressed HUN: ${it.logReason}", it.shouldInterrupt)
         }
 
@@ -876,6 +900,7 @@
 
     protected fun assertShouldFsi(entry: NotificationEntry) =
         provider.makeUnloggedFullScreenIntentDecision(entry).let {
+            provider.logFullScreenIntentDecision(it)
             assertTrue("unexpected suppressed FSI: ${it.logReason}", it.shouldInterrupt)
         }
 
@@ -884,10 +909,11 @@
         expectWouldInterruptWithoutDnd: Boolean? = null
     ) =
         provider.makeUnloggedFullScreenIntentDecision(entry).let {
+            provider.logFullScreenIntentDecision(it)
             assertFalse("unexpected unsuppressed FSI: ${it.logReason}", it.shouldInterrupt)
             if (expectWouldInterruptWithoutDnd != null) {
                 assertEquals(
-                    "unexpected unsuppressed-without-DND FSI: ${it.logReason}",
+                    "unexpected wouldInterruptWithoutDnd for FSI: ${it.logReason}",
                     expectWouldInterruptWithoutDnd,
                     it.wouldInterruptWithoutDnd
                 )
@@ -895,22 +921,35 @@
         }
 
     protected class EntryBuilder(val context: Context) {
-        var importance = IMPORTANCE_DEFAULT
-        var suppressedVisualEffects: Int? = null
-        var whenMs: Long? = null
-        var visibilityOverride: Int? = null
-        var hasFsi = false
-        var canBubble: Boolean? = null
-        var isBubble = false
-        var hasBubbleMetadata = false
+        // Set on BubbleMetadata:
         var bubbleIsShortcut = false
-        var bubbleSuppressesNotification: Boolean? = null
+        var bubbleSuppressesNotification = false
+
+        // Set on Notification.Builder:
+        var whenMs: Long? = null
         var isGrouped = false
-        var isGroupSummary: Boolean? = null
+        var isGroupSummary = false
         var groupAlertBehavior: Int? = null
-        var hasJustLaunchedFsi = false
+        var hasBubbleMetadata = false
+        var hasFsi = false
+
+        // Set on Notification:
+        var isForegroundService = false
+        var isUserInitiatedJob = false
+        var isBubble = false
         var isStickyAndNotDemoted = false
-        var packageSuspended: Boolean? = null
+
+        // Set on NotificationEntryBuilder:
+        var importance = IMPORTANCE_DEFAULT
+        var canBubble: Boolean? = null
+
+        // Set on NotificationEntry:
+        var hasJustLaunchedFsi = false
+
+        // Set on ModifiedRankingBuilder:
+        var packageSuspended = false
+        var visibilityOverride: Int? = null
+        var suppressedVisualEffects: Int? = null
 
         private fun buildBubbleMetadata(): BubbleMetadata {
             val builder =
@@ -928,71 +967,87 @@
                     )
                 }
 
-            bubbleSuppressesNotification?.let { builder.setSuppressNotification(it) }
+            if (bubbleSuppressesNotification) {
+                builder.setSuppressNotification(true)
+            }
 
             return builder.build()
         }
 
         fun build() =
             Notification.Builder(context, TEST_CHANNEL_ID)
-                .apply {
-                    setContentTitle(TEST_CONTENT_TITLE)
-                    setContentText(TEST_CONTENT_TEXT)
+                .also { nb ->
+                    nb.setContentTitle(TEST_CONTENT_TITLE)
+                    nb.setContentText(TEST_CONTENT_TEXT)
 
-                    if (hasFsi) {
-                        setFullScreenIntent(mock(), /* highPriority = */ true)
-                    }
-
-                    whenMs?.let { setWhen(it) }
-
-                    if (hasBubbleMetadata) {
-                        setBubbleMetadata(buildBubbleMetadata())
-                    }
+                    whenMs?.let { nb.setWhen(it) }
 
                     if (isGrouped) {
-                        setGroup(TEST_GROUP_KEY)
+                        nb.setGroup(TEST_GROUP_KEY)
                     }
 
-                    isGroupSummary?.let { setGroupSummary(it) }
+                    if (isGroupSummary) {
+                        nb.setGroupSummary(true)
+                    }
 
-                    groupAlertBehavior?.let { setGroupAlertBehavior(it) }
+                    groupAlertBehavior?.let { nb.setGroupAlertBehavior(it) }
+
+                    if (hasBubbleMetadata) {
+                        nb.setBubbleMetadata(buildBubbleMetadata())
+                    }
+
+                    if (hasFsi) {
+                        nb.setFullScreenIntent(mock(), /* highPriority = */ true)
+                    }
                 }
                 .build()
-                .apply {
+                .also { n ->
+                    if (isForegroundService) {
+                        n.flags = n.flags or FLAG_FOREGROUND_SERVICE
+                    }
+
+                    if (isUserInitiatedJob) {
+                        n.flags = n.flags or FLAG_USER_INITIATED_JOB
+                    }
+
                     if (isBubble) {
-                        flags = flags or FLAG_BUBBLE
+                        n.flags = n.flags or FLAG_BUBBLE
                     }
 
                     if (isStickyAndNotDemoted) {
-                        flags = flags or FLAG_FSI_REQUESTED_BUT_DENIED
+                        n.flags = n.flags or FLAG_FSI_REQUESTED_BUT_DENIED
                     }
                 }
                 .let { NotificationEntryBuilder().setNotification(it) }
-                .apply {
-                    setPkg(TEST_PACKAGE)
-                    setOpPkg(TEST_PACKAGE)
-                    setTag(TEST_TAG)
+                .also { neb ->
+                    neb.setPkg(TEST_PACKAGE)
+                    neb.setOpPkg(TEST_PACKAGE)
+                    neb.setTag(TEST_TAG)
 
-                    setImportance(importance)
-                    setChannel(NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_NAME, importance))
+                    neb.setImportance(importance)
+                    neb.setChannel(
+                        NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_NAME, importance)
+                    )
 
-                    canBubble?.let { setCanBubble(it) }
+                    canBubble?.let { neb.setCanBubble(it) }
                 }
                 .build()!!
-                .also {
+                .also { ne ->
                     if (hasJustLaunchedFsi) {
-                        it.notifyFullScreenIntentLaunched()
+                        ne.notifyFullScreenIntentLaunched()
                     }
 
                     if (isStickyAndNotDemoted) {
-                        assertFalse(it.isDemoted)
+                        assertFalse(ne.isDemoted)
                     }
 
-                    modifyRanking(it)
-                        .apply {
-                            suppressedVisualEffects?.let { setSuppressedVisualEffects(it) }
-                            visibilityOverride?.let { setVisibilityOverride(it) }
-                            packageSuspended?.let { setSuspended(it) }
+                    modifyRanking(ne)
+                        .also { mrb ->
+                            if (packageSuspended) {
+                                mrb.setSuspended(true)
+                            }
+                            visibilityOverride?.let { mrb.setVisibilityOverride(it) }
+                            suppressedVisualEffects?.let { mrb.setSuppressedVisualEffects(it) }
                         }
                         .build()
                 }
@@ -1013,6 +1068,7 @@
     }
 
     protected fun buildBubbleEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry {
+        isBubble = true
         canBubble = true
         hasBubbleMetadata = true
         run(block)