Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +12 −15 Original line number Diff line number Diff line Loading @@ -43,9 +43,9 @@ import com.android.systemui.util.time.SystemClock 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 Loading Loading @@ -87,16 +87,13 @@ class PeekDisabledSuppressor( 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() } Loading Loading @@ -128,14 +125,14 @@ class PeekDndSuppressor() : } 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 Loading @@ -144,7 +141,7 @@ class PeekDeviceNotInUseSuppressor( } 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` Loading @@ -165,21 +162,21 @@ class PeekOldWhenSuppressor(private val systemClock: SystemClock) : } 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 } Loading @@ -198,12 +195,12 @@ class HunJustLaunchedFsiSuppressor() : } 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) Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt +18 −5 Original line number Diff line number Diff line Loading @@ -50,19 +50,32 @@ class FullScreenIntentDecisionProvider( 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"), Loading @@ -71,7 +84,7 @@ class FullScreenIntentDecisionProvider( 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) } Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt 0 → 100644 +93 −0 Original line number Diff line number Diff line /* * 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" packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +110 −104 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.interruption 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 Loading Loading @@ -46,7 +47,7 @@ constructor( 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, Loading @@ -58,9 +59,32 @@ constructor( 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 Loading @@ -69,6 +93,12 @@ constructor( override val logReason get() = fsiDecision.logReason val shouldLog get() = fsiDecision.shouldLog val isWarning get() = fsiDecision.isWarning } private val fullScreenIntentDecisionProvider = Loading Loading @@ -139,137 +169,113 @@ constructor( 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. } private fun makeHeadsUpDecision(entry: NotificationEntry): DecisionImpl { if (statusBarStateController.isDozing) { return makePulseDecision(entry) } else { return makePeekDecision(entry) } } 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") if (decision !is FullScreenIntentDecisionImpl) { Log.wtf(TAG, "FSI decision $decision was not created by this class") return } 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") if (decision.hasBeenLogged) { Log.wtf(TAG, "FSI decision $decision has already been logged") return } 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") } decision.hasBeenLogged = true private fun logHeadsUpDecision(entry: NotificationEntry, decision: DecisionImpl) { // Not yet implemented. if (!decision.shouldLog) { return } private fun logBubbleDecision(entry: NotificationEntry, decision: DecisionImpl) { // Not yet implemented. logger.logFullScreenIntentDecision(decision.entry, decision, decision.isWarning) } private fun makeFullScreenIntentDecision(entry: NotificationEntry): FullScreenIntentDecision { val wouldHeadsUp = makeUnloggedHeadsUpDecision(entry).shouldInterrupt val fsiDecision = fullScreenIntentDecisionProvider.makeFullScreenIntentDecision(entry, wouldHeadsUp) return FullScreenIntentDecisionImpl(fsiDecision) } private fun checkSuppressInterruptions(entry: NotificationEntry) = legacySuppressors .firstOrNull { it.suppressInterruptions(entry) } ?.let { LoggableDecision.suppressed(it, "suppressInterruptions") } private fun checkSuppressors(entry: NotificationEntry) = legacySuppressors.firstOrNull { it.suppressInterruptions(entry) } private fun checkSuppressAwakeInterruptions(entry: NotificationEntry) = legacySuppressors .firstOrNull { it.suppressAwakeInterruptions(entry) } ?.let { LoggableDecision.suppressed(it, "suppressAwakeInterruptions") } private fun checkAwakeSuppressors(entry: NotificationEntry) = legacySuppressors.firstOrNull { it.suppressAwakeInterruptions(entry) } private fun checkSuppressAwakeHeadsUp(entry: NotificationEntry) = legacySuppressors .firstOrNull { it.suppressAwakeHeadsUp(entry) } ?.let { LoggableDecision.suppressed(it, "suppressAwakeHeadsUp") } private fun checkAwakeHeadsUpSuppressors(entry: NotificationEntry) = legacySuppressors.firstOrNull { it.suppressAwakeHeadsUp(entry) } private fun checkConditions(type: VisualInterruptionType) = conditions .firstOrNull { it.types.contains(type) && it.shouldSuppress() } ?.let { LoggableDecision.suppressed(it) } 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" packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt +1 −1 Original line number Diff line number Diff line Loading @@ -44,7 +44,7 @@ class NotificationInterruptStateProviderWrapperTest : VisualInterruptionDecision statusBarStateController, keyguardStateController, headsUpManager, logger, oldLogger, mainHandler, flags, keyguardNotificationVisibilityProvider, Loading Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +12 −15 Original line number Diff line number Diff line Loading @@ -43,9 +43,9 @@ import com.android.systemui.util.time.SystemClock 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 Loading Loading @@ -87,16 +87,13 @@ class PeekDisabledSuppressor( 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() } Loading Loading @@ -128,14 +125,14 @@ class PeekDndSuppressor() : } 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 Loading @@ -144,7 +141,7 @@ class PeekDeviceNotInUseSuppressor( } 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` Loading @@ -165,21 +162,21 @@ class PeekOldWhenSuppressor(private val systemClock: SystemClock) : } 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 } Loading @@ -198,12 +195,12 @@ class HunJustLaunchedFsiSuppressor() : } 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) Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt +18 −5 Original line number Diff line number Diff line Loading @@ -50,19 +50,32 @@ class FullScreenIntentDecisionProvider( 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"), Loading @@ -71,7 +84,7 @@ class FullScreenIntentDecisionProvider( 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) } Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt 0 → 100644 +93 −0 Original line number Diff line number Diff line /* * 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"
packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +110 −104 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.interruption 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 Loading Loading @@ -46,7 +47,7 @@ constructor( 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, Loading @@ -58,9 +59,32 @@ constructor( 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 Loading @@ -69,6 +93,12 @@ constructor( override val logReason get() = fsiDecision.logReason val shouldLog get() = fsiDecision.shouldLog val isWarning get() = fsiDecision.isWarning } private val fullScreenIntentDecisionProvider = Loading Loading @@ -139,137 +169,113 @@ constructor( 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. } private fun makeHeadsUpDecision(entry: NotificationEntry): DecisionImpl { if (statusBarStateController.isDozing) { return makePulseDecision(entry) } else { return makePeekDecision(entry) } } 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") if (decision !is FullScreenIntentDecisionImpl) { Log.wtf(TAG, "FSI decision $decision was not created by this class") return } 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") if (decision.hasBeenLogged) { Log.wtf(TAG, "FSI decision $decision has already been logged") return } 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") } decision.hasBeenLogged = true private fun logHeadsUpDecision(entry: NotificationEntry, decision: DecisionImpl) { // Not yet implemented. if (!decision.shouldLog) { return } private fun logBubbleDecision(entry: NotificationEntry, decision: DecisionImpl) { // Not yet implemented. logger.logFullScreenIntentDecision(decision.entry, decision, decision.isWarning) } private fun makeFullScreenIntentDecision(entry: NotificationEntry): FullScreenIntentDecision { val wouldHeadsUp = makeUnloggedHeadsUpDecision(entry).shouldInterrupt val fsiDecision = fullScreenIntentDecisionProvider.makeFullScreenIntentDecision(entry, wouldHeadsUp) return FullScreenIntentDecisionImpl(fsiDecision) } private fun checkSuppressInterruptions(entry: NotificationEntry) = legacySuppressors .firstOrNull { it.suppressInterruptions(entry) } ?.let { LoggableDecision.suppressed(it, "suppressInterruptions") } private fun checkSuppressors(entry: NotificationEntry) = legacySuppressors.firstOrNull { it.suppressInterruptions(entry) } private fun checkSuppressAwakeInterruptions(entry: NotificationEntry) = legacySuppressors .firstOrNull { it.suppressAwakeInterruptions(entry) } ?.let { LoggableDecision.suppressed(it, "suppressAwakeInterruptions") } private fun checkAwakeSuppressors(entry: NotificationEntry) = legacySuppressors.firstOrNull { it.suppressAwakeInterruptions(entry) } private fun checkSuppressAwakeHeadsUp(entry: NotificationEntry) = legacySuppressors .firstOrNull { it.suppressAwakeHeadsUp(entry) } ?.let { LoggableDecision.suppressed(it, "suppressAwakeHeadsUp") } private fun checkAwakeHeadsUpSuppressors(entry: NotificationEntry) = legacySuppressors.firstOrNull { it.suppressAwakeHeadsUp(entry) } private fun checkConditions(type: VisualInterruptionType) = conditions .firstOrNull { it.types.contains(type) && it.shouldSuppress() } ?.let { LoggableDecision.suppressed(it) } 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"
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt +1 −1 Original line number Diff line number Diff line Loading @@ -44,7 +44,7 @@ class NotificationInterruptStateProviderWrapperTest : VisualInterruptionDecision statusBarStateController, keyguardStateController, headsUpManager, logger, oldLogger, mainHandler, flags, keyguardNotificationVisibilityProvider, Loading