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

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

Merge "Implement FSI suppression logic" into main

parents 75ef63db 3813741b
Loading
Loading
Loading
Loading
+158 −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 android.app.NotificationManager.IMPORTANCE_HIGH
import android.os.PowerManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_DEVICE_DREAMING
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_DEVICE_NOT_INTERACTIVE
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_DEVICE_NOT_PROVISIONED
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_KEYGUARD_OCCLUDED
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_KEYGUARD_SHOWING
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.FSI_LOCKED_SHADE
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_EXPECTED_TO_HUN
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_NOT_IMPORTANT_ENOUGH
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_NO_FULL_SCREEN_INTENT
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_NO_HUN_OR_KEYGUARD
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_PACKAGE_SUSPENDED
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SHOW_STICKY_HUN
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSED_BY_DND
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSED_ONLY_BY_DND
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA
import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.KeyguardStateController

class FullScreenIntentDecisionProvider(
    private val deviceProvisionedController: DeviceProvisionedController,
    private val keyguardStateController: KeyguardStateController,
    private val powerManager: PowerManager,
    private val statusBarStateController: StatusBarStateController
) {
    interface Decision {
        val shouldFsi: Boolean
        val wouldFsiWithoutDnd: Boolean
        val logReason: String
    }

    private enum class DecisionImpl(
        override val shouldFsi: Boolean,
        override val logReason: String,
        override val wouldFsiWithoutDnd: Boolean = shouldFsi,
        val supersedesDnd: Boolean = false
    ) : Decision {
        NO_FSI_NO_FULL_SCREEN_INTENT(false, "no full-screen intent", supersedesDnd = true),
        NO_FSI_SHOW_STICKY_HUN(false, "full-screen intents are disabled", supersedesDnd = true),
        NO_FSI_NOT_IMPORTANT_ENOUGH(false, "not important enough"),
        NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(false, "suppressive group alert behavior"),
        NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(false, "suppressive bubble metadata"),
        NO_FSI_PACKAGE_SUSPENDED(false, "package suspended"),
        FSI_DEVICE_NOT_INTERACTIVE(true, "device is not interactive"),
        FSI_DEVICE_DREAMING(true, "device is dreaming"),
        FSI_KEYGUARD_SHOWING(true, "keyguard is showing"),
        NO_FSI_EXPECTED_TO_HUN(false, "expected to heads-up instead"),
        FSI_KEYGUARD_OCCLUDED(true, "keyguard is occluded"),
        FSI_LOCKED_SHADE(true, "locked shade"),
        FSI_DEVICE_NOT_PROVISIONED(true, "device not provisioned"),
        NO_FSI_NO_HUN_OR_KEYGUARD(false, "no HUN or keyguard"),
        NO_FSI_SUPPRESSED_BY_DND(false, "suppressed by DND", wouldFsiWithoutDnd = false),
        NO_FSI_SUPPRESSED_ONLY_BY_DND(false, "suppressed only by DND", wouldFsiWithoutDnd = true)
    }

    fun makeFullScreenIntentDecision(entry: NotificationEntry, couldHeadsUp: Boolean): Decision {
        val reasonWithoutDnd = makeDecisionWithoutDnd(entry, couldHeadsUp)

        val suppressedWithoutDnd = !reasonWithoutDnd.shouldFsi
        val suppressedByDnd = entry.shouldSuppressFullScreenIntent()

        val reasonWithDnd =
            when {
                reasonWithoutDnd.supersedesDnd -> reasonWithoutDnd
                suppressedByDnd && !suppressedWithoutDnd -> NO_FSI_SUPPRESSED_ONLY_BY_DND
                suppressedByDnd -> NO_FSI_SUPPRESSED_BY_DND
                else -> reasonWithoutDnd
            }

        return reasonWithDnd
    }

    private fun makeDecisionWithoutDnd(
        entry: NotificationEntry,
        couldHeadsUp: Boolean
    ): DecisionImpl {
        val sbn = entry.sbn
        val notification = sbn.notification!!

        if (notification.fullScreenIntent == null) {
            return if (entry.isStickyAndNotDemoted) {
                NO_FSI_SHOW_STICKY_HUN
            } else {
                NO_FSI_NO_FULL_SCREEN_INTENT
            }
        }

        if (entry.importance < IMPORTANCE_HIGH) {
            return NO_FSI_NOT_IMPORTANT_ENOUGH
        }

        if (sbn.isGroup && notification.suppressAlertingDueToGrouping()) {
            return NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
        }

        val bubbleMetadata = notification.bubbleMetadata
        if (bubbleMetadata != null && bubbleMetadata.isNotificationSuppressed) {
            return NO_FSI_SUPPRESSIVE_BUBBLE_METADATA
        }

        if (entry.ranking.isSuspended) {
            return NO_FSI_PACKAGE_SUSPENDED
        }

        if (!powerManager.isInteractive) {
            return FSI_DEVICE_NOT_INTERACTIVE
        }

        if (statusBarStateController.isDreaming) {
            return FSI_DEVICE_DREAMING
        }

        if (statusBarStateController.state == KEYGUARD) {
            return FSI_KEYGUARD_SHOWING
        }

        if (couldHeadsUp) {
            return NO_FSI_EXPECTED_TO_HUN
        }

        if (keyguardStateController.isShowing) {
            return if (keyguardStateController.isOccluded) {
                FSI_KEYGUARD_OCCLUDED
            } else {
                FSI_LOCKED_SHADE
            }
        }

        if (!deviceProvisionedController.isDeviceProvisioned) {
            return FSI_DEVICE_NOT_PROVISIONED
        }

        return NO_FSI_NO_HUN_OR_KEYGUARD
    }
}
+46 −49
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ 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
@@ -30,7 +29,9 @@ import com.android.systemui.statusbar.notification.interruption.VisualInterrupti
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
@@ -40,9 +41,11 @@ class VisualInterruptionDecisionProviderImpl
constructor(
    private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
    private val batteryController: BatteryController,
    deviceProvisionedController: DeviceProvisionedController,
    private val globalSettings: GlobalSettings,
    private val headsUpManager: HeadsUpManager,
    private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
    keyguardStateController: KeyguardStateController,
    private val logger: NotificationInterruptLogger,
    @Main private val mainHandler: Handler,
    private val powerManager: PowerManager,
@@ -50,6 +53,36 @@ constructor(
    private val systemClock: SystemClock,
    private val userTracker: UserTracker,
) : VisualInterruptionDecisionProvider {
    private class DecisionImpl(
        override val shouldInterrupt: Boolean,
        override val logReason: String
    ) : Decision

    private class FullScreenIntentDecisionImpl(
        private val fsiDecision: FullScreenIntentDecisionProvider.Decision
    ) : FullScreenIntentDecision {
        override val shouldInterrupt
            get() = fsiDecision.shouldFsi

        override val wouldInterruptWithoutDnd
            get() = fsiDecision.wouldFsiWithoutDnd

        override val logReason
            get() = fsiDecision.logReason
    }

    private val fullScreenIntentDecisionProvider =
        FullScreenIntentDecisionProvider(
            deviceProvisionedController,
            keyguardStateController,
            powerManager,
            statusBarStateController
        )

    private val legacySuppressors = mutableSetOf<NotificationInterruptSuppressor>()
    private val conditions = mutableListOf<VisualInterruptionCondition>()
    private val filters = mutableListOf<VisualInterruptionFilter>()

    private var started = false

    override fun start() {
@@ -76,24 +109,6 @@ constructor(
        started = true
    }

    private class DecisionImpl(
        override val shouldInterrupt: Boolean,
        override val logReason: String
    ) : Decision

    private class FullScreenIntentDecisionImpl(
        override val shouldInterrupt: Boolean,
        override val wouldInterruptWithoutDnd: Boolean,
        override val logReason: String,
        val originalEntry: NotificationEntry,
    ) : FullScreenIntentDecision {
        var hasBeenLogged = false
    }

    private val legacySuppressors = mutableSetOf<NotificationInterruptSuppressor>()
    private val conditions = mutableListOf<VisualInterruptionCondition>()
    private val filters = mutableListOf<VisualInterruptionFilter>()

    override fun addLegacySuppressor(suppressor: NotificationInterruptSuppressor) {
        legacySuppressors.add(suppressor)
    }
@@ -132,32 +147,21 @@ constructor(
        return makeHeadsUpDecision(entry).also { logHeadsUpDecision(entry, it) }
    }

    override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision {
        check(started)
        return makeBubbleDecision(entry).also { logBubbleDecision(entry, it) }
    }

    override fun makeUnloggedFullScreenIntentDecision(
        entry: NotificationEntry
    ): FullScreenIntentDecision {
        check(started)
        return makeFullScreenDecision(entry)
        return makeFullScreenIntentDecision(entry)
    }

    override fun logFullScreenIntentDecision(decision: FullScreenIntentDecision) {
        check(started)
        val decisionImpl =
            decision as? FullScreenIntentDecisionImpl
                ?: run {
                    Log.wtf(TAG, "Wrong subclass of FullScreenIntentDecision: $decision")
                    return
                }
        if (decision.hasBeenLogged) {
            Log.wtf(TAG, "Already logged decision: $decision")
            return
        }
        logFullScreenIntentDecision(decisionImpl)
        decision.hasBeenLogged = true
    }

    override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision {
        check(started)
        return makeBubbleDecision(entry).also { logBubbleDecision(entry, it) }
        // Not yet implemented.
    }

    private fun makeHeadsUpDecision(entry: NotificationEntry): DecisionImpl {
@@ -234,16 +238,6 @@ constructor(
        return DecisionImpl(shouldInterrupt = true, logReason = "not suppressed")
    }

    private fun makeFullScreenDecision(entry: NotificationEntry): FullScreenIntentDecisionImpl {
        // Not yet implemented.
        return FullScreenIntentDecisionImpl(
            shouldInterrupt = true,
            wouldInterruptWithoutDnd = true,
            logReason = "FSI logic not yet implemented in VisualInterruptionDecisionProviderImpl",
            originalEntry = entry
        )
    }

    private fun logHeadsUpDecision(entry: NotificationEntry, decision: DecisionImpl) {
        // Not yet implemented.
    }
@@ -252,8 +246,11 @@ constructor(
        // Not yet implemented.
    }

    private fun logFullScreenIntentDecision(decision: FullScreenIntentDecisionImpl) {
        // Not yet implemented.
    private fun makeFullScreenIntentDecision(entry: NotificationEntry): FullScreenIntentDecision {
        val wouldHeadsUp = makeUnloggedHeadsUpDecision(entry).shouldInterrupt
        val fsiDecision =
            fullScreenIntentDecisionProvider.makeFullScreenIntentDecision(entry, wouldHeadsUp)
        return FullScreenIntentDecisionImpl(fsiDecision)
    }

    private fun checkSuppressors(entry: NotificationEntry) =
+14 −0
Original line number Diff line number Diff line
@@ -32,9 +32,11 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
        VisualInterruptionDecisionProviderImpl(
            ambientDisplayConfiguration,
            batteryController,
            deviceProvisionedController,
            globalSettings,
            headsUpManager,
            keyguardNotificationVisibilityProvider,
            keyguardStateController,
            logger,
            mainHandler,
            powerManager,
@@ -50,6 +52,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            assertPeekNotSuppressed()
            assertPulseNotSuppressed()
            assertBubbleNotSuppressed()
            assertFsiNotSuppressed()
        }
    }

@@ -59,6 +62,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            assertPeekNotSuppressed()
            assertPulseNotSuppressed()
            assertBubbleNotSuppressed()
            assertFsiNotSuppressed()
        }
    }

@@ -68,6 +72,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            assertPeekSuppressed()
            assertPulseNotSuppressed()
            assertBubbleNotSuppressed()
            assertFsiNotSuppressed()
        }
    }

@@ -77,6 +82,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            assertPeekSuppressed()
            assertPulseNotSuppressed()
            assertBubbleNotSuppressed()
            assertFsiNotSuppressed()
        }
    }

@@ -86,6 +92,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            assertPeekNotSuppressed()
            assertPulseSuppressed()
            assertBubbleNotSuppressed()
            assertFsiNotSuppressed()
        }
    }

@@ -95,6 +102,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            assertPeekNotSuppressed()
            assertPulseSuppressed()
            assertBubbleNotSuppressed()
            assertFsiNotSuppressed()
        }
    }

@@ -104,6 +112,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            assertPeekNotSuppressed()
            assertPulseNotSuppressed()
            assertBubbleSuppressed()
            assertFsiNotSuppressed()
        }
    }

@@ -113,6 +122,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            assertPeekNotSuppressed()
            assertPulseNotSuppressed()
            assertBubbleSuppressed()
            assertFsiNotSuppressed()
        }
    }

@@ -193,6 +203,10 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
        assertShouldBubble(buildBubbleEntry())
    }

    private fun assertFsiNotSuppressed() {
        forEachFsiState { assertShouldFsi(buildFsiEntry()) }
    }

    private fun withCondition(condition: VisualInterruptionCondition, block: () -> Unit) {
        provider.addCondition(condition)
        block()
+288 −13

File changed.

Preview size limit exceeded, changes collapsed.

+31 −0
Original line number Diff line number Diff line
package com.android.systemui.statusbar.policy

class FakeDeviceProvisionedController : DeviceProvisionedController {
    @JvmField var deviceProvisioned = true

    override fun addCallback(listener: DeviceProvisionedController.DeviceProvisionedListener) {
        TODO("Not yet implemented")
    }

    override fun removeCallback(listener: DeviceProvisionedController.DeviceProvisionedListener) {
        TODO("Not yet implemented")
    }

    override fun isDeviceProvisioned() = deviceProvisioned

    override fun getCurrentUser(): Int {
        TODO("Not yet implemented")
    }

    override fun isUserSetup(user: Int): Boolean {
        TODO("Not yet implemented")
    }

    override fun isCurrentUserSetup(): Boolean {
        TODO("Not yet implemented")
    }

    override fun isFrpActive(): Boolean {
        TODO("Not yet implemented")
    }
}
Loading