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

Commit 3813741b authored by Julia Tuttle's avatar Julia Tuttle
Browse files

Implement FSI suppression logic

Bug: 261728888
Test: atest NotificationInterruptStateProviderWrapperTest
Test: atest VisualInterruptionDecisionProviderImplTest
Flag: ACONFIG com.android.systemui.visual_interruptions_refactor DEVELOPMENT
Change-Id: I6866afbc9cbf1b9ca500afcaf5e1ab4d120cbdad
parent d9500e00
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