Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5325f2b2 authored by Julia Tuttle's avatar Julia Tuttle Committed by Android (Google) Code Review
Browse files

Merge changes Iff50fbf8,Ibfd590be,Ibc4484bb into main

* changes:
  Add logging to LogBuffer
  Refactor VisualInterruptionDecisionProviderImpl
  Improve tests and add missing test cases
parents f5eae914 63ad0900
Loading
Loading
Loading
Loading
+12 −15
Original line number Diff line number Diff line
@@ -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
@@ -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()
}

@@ -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
@@ -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`

@@ -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
}

@@ -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)
+18 −5
Original line number Diff line number Diff line
@@ -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"),
@@ -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)
    }
+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"
+110 −104
Original line number Diff line number Diff line
@@ -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
@@ -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,
@@ -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

@@ -69,6 +93,12 @@ constructor(

        override val logReason
            get() = fsiDecision.logReason

        val shouldLog
            get() = fsiDecision.shouldLog

        val isWarning
            get() = fsiDecision.isWarning
    }

    private val fullScreenIntentDecisionProvider =
@@ -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"
+1 −1
Original line number Diff line number Diff line
@@ -44,7 +44,7 @@ class NotificationInterruptStateProviderWrapperTest : VisualInterruptionDecision
                statusBarStateController,
                keyguardStateController,
                headsUpManager,
                logger,
                oldLogger,
                mainHandler,
                flags,
                keyguardNotificationVisibilityProvider,
Loading