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

Commit 785b0398 authored by Lyn Han's avatar Lyn Han Committed by Android (Google) Code Review
Browse files

Merge "Suppress HUNs after avalanche, with allowlist" into main

parents ffd31b17 6fc4d0c9
Loading
Loading
Loading
Loading
+71 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2024 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.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject

// Class to track avalanche trigger event time.
@SysUISingleton
class AvalancheProvider
@Inject
constructor(
        private val broadcastDispatcher: BroadcastDispatcher,
        private val logger: VisualInterruptionDecisionLogger,
) {
    val TAG = "AvalancheProvider"
    val timeoutMs = 120000
    var startTime: Long = 0L

    private val avalancheTriggerIntents = mutableSetOf(
            Intent.ACTION_AIRPLANE_MODE_CHANGED,
            Intent.ACTION_BOOT_COMPLETED,
            Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
            Intent.ACTION_USER_SWITCHED
    )

    private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action in avalancheTriggerIntents) {

                // Ignore when airplane mode turned on
                if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED
                        && intent.getBooleanExtra(/* name= */ "state", /* defaultValue */ false)) {
                    Log.d(TAG, "broadcastReceiver: ignore airplane mode on")
                    return
                }
                Log.d(TAG, "broadcastReceiver received intent.action=" + intent.action)
                startTime = System.currentTimeMillis()
            }
        }
    }

    fun register() {
        val intentFilter = IntentFilter()
        for (intent in avalancheTriggerIntents) {
            intentFilter.addAction(intent)
        }
        broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter)
    }
}
 No newline at end of file
+68 −0
Original line number Original line Diff line number Diff line
@@ -16,7 +16,10 @@


package com.android.systemui.statusbar.notification.interruption
package com.android.systemui.statusbar.notification.interruption


import android.app.Notification
import android.app.Notification.BubbleMetadata
import android.app.Notification.BubbleMetadata
import android.app.Notification.CATEGORY_EVENT
import android.app.Notification.CATEGORY_REMINDER
import android.app.Notification.VISIBILITY_PRIVATE
import android.app.Notification.VISIBILITY_PRIVATE
import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.NotificationManager.IMPORTANCE_HIGH
@@ -224,3 +227,68 @@ class AlertKeyguardVisibilitySuppressor(
    override fun shouldSuppress(entry: NotificationEntry) =
    override fun shouldSuppress(entry: NotificationEntry) =
        keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
        keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
}
}


class AvalancheSuppressor(
    private val avalancheProvider: AvalancheProvider,
    private val systemClock: SystemClock,
) : VisualInterruptionFilter(
        types = setOf(PEEK, PULSE),
        reason = "avalanche",
    ) {
    val TAG = "AvalancheSuppressor"

    enum class State {
        ALLOW_CONVERSATION_AFTER_AVALANCHE,
        ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME,
        ALLOW_CALLSTYLE,
        ALLOW_CATEGORY_REMINDER,
        ALLOW_CATEGORY_EVENT,
        ALLOW_FSI_WITH_PERMISSION_ON,
        ALLOW_COLORIZED,
        SUPPRESS
    }

    override fun shouldSuppress(entry: NotificationEntry): Boolean {
        val timeSinceAvalanche = systemClock.currentTimeMillis() - avalancheProvider.startTime
        val isActive = timeSinceAvalanche < avalancheProvider.timeoutMs
        val state = allow(entry)
        val suppress = isActive && state == State.SUPPRESS
        reason = "avalanche suppress=$suppress isActive=$isActive state=$state"
        return suppress
    }

    fun allow(entry: NotificationEntry): State  {
        if (
            entry.ranking.isConversation &&
                entry.sbn.notification.`when` > avalancheProvider.startTime
        ) {
            return State.ALLOW_CONVERSATION_AFTER_AVALANCHE
        }

        if (entry.channel?.isImportantConversation == true) {
            return State.ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME
        }

        if (entry.sbn.notification.isStyle(Notification.CallStyle::class.java)) {
            return State.ALLOW_CALLSTYLE
        }

        if (entry.sbn.notification.category == CATEGORY_REMINDER) {
            return State.ALLOW_CATEGORY_REMINDER
        }

        if (entry.sbn.notification.category == CATEGORY_EVENT) {
            return State.ALLOW_CATEGORY_EVENT
        }

        if (entry.sbn.notification.fullScreenIntent != null) {
            return State.ALLOW_FSI_WITH_PERMISSION_ON
        }

        if (entry.sbn.notification.isColorized) {
            return State.ALLOW_COLORIZED
        }
        return State.SUPPRESS
    }
}
+23 −15
Original line number Original line Diff line number Diff line
@@ -33,6 +33,7 @@ import com.android.systemui.statusbar.notification.interruption.VisualInterrupti
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -60,7 +61,10 @@ constructor(
        private val systemClock: SystemClock,
        private val systemClock: SystemClock,
        private val uiEventLogger: UiEventLogger,
        private val uiEventLogger: UiEventLogger,
        private val userTracker: UserTracker,
        private val userTracker: UserTracker,
        private val avalancheProvider: AvalancheProvider

) : VisualInterruptionDecisionProvider {
) : VisualInterruptionDecisionProvider {

    init {
    init {
        check(!VisualInterruptionRefactor.isUnexpectedlyInLegacyMode())
        check(!VisualInterruptionRefactor.isUnexpectedlyInLegacyMode())
    }
    }
@@ -166,6 +170,10 @@ constructor(
        addFilter(HunJustLaunchedFsiSuppressor())
        addFilter(HunJustLaunchedFsiSuppressor())
        addFilter(AlertKeyguardVisibilitySuppressor(keyguardNotificationVisibilityProvider))
        addFilter(AlertKeyguardVisibilitySuppressor(keyguardNotificationVisibilityProvider))


        if (NotificationAvalancheSuppression.isEnabled) {
            addFilter(AvalancheSuppressor(avalancheProvider, systemClock))
            avalancheProvider.register()
        }
        started = true
        started = true
    }
    }


+1 −1
Original line number Original line Diff line number Diff line
@@ -85,7 +85,7 @@ abstract class VisualInterruptionCondition(
/** A reason why visual interruptions might be suppressed based on the notification. */
/** A reason why visual interruptions might be suppressed based on the notification. */
abstract class VisualInterruptionFilter(
abstract class VisualInterruptionFilter(
    override val types: Set<VisualInterruptionType>,
    override val types: Set<VisualInterruptionType>,
    override val reason: String,
    override var reason: String,
    override val uiEventId: UiEventEnum? = null,
    override val uiEventId: UiEventEnum? = null,
    override val eventLogData: EventLogData? = null
    override val eventLogData: EventLogData? = null
) : VisualInterruptionSuppressor {
) : VisualInterruptionSuppressor {
+112 −0
Original line number Original line Diff line number Diff line
@@ -16,6 +16,9 @@


package com.android.systemui.statusbar.notification.interruption
package com.android.systemui.statusbar.notification.interruption


import android.app.Notification.CATEGORY_EVENT
import android.app.Notification.CATEGORY_REMINDER
import android.app.NotificationManager
import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
@@ -47,6 +50,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            systemClock,
            systemClock,
            uiEventLogger,
            uiEventLogger,
            userTracker,
            userTracker,
            avalancheProvider
        )
        )
    }
    }


@@ -70,6 +74,114 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
        }
        }
    }
    }


    // Avalanche tests are in VisualInterruptionDecisionProviderImplTest
    // instead of VisualInterruptionDecisionProviderTestBase
    // because avalanche code is based on the suppression refactor.

    @Test
    fun testAvalancheFilter_duringAvalanche_allowConversationFromAfterEvent() {
        avalancheProvider.startTime = whenAgo(10)

        withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
            ensurePeekState()
            assertShouldHeadsUp(buildEntry {
                importance = NotificationManager.IMPORTANCE_HIGH
                isConversation = true
                isImportantConversation = false
                whenMs = whenAgo(5)
            })
        }
    }

    @Test
    fun testAvalancheFilter_duringAvalanche_suppressConversationFromBeforeEvent() {
        avalancheProvider.startTime = whenAgo(10)

        withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
            ensurePeekState()
            assertShouldNotHeadsUp(buildEntry {
                importance = NotificationManager.IMPORTANCE_DEFAULT
                isConversation = true
                isImportantConversation = false
                whenMs = whenAgo(15)
            })
        }
    }

    @Test
    fun testAvalancheFilter_duringAvalanche_allowHighPriorityConversation() {
        avalancheProvider.startTime = whenAgo(10)

        withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
            ensurePeekState()
            assertShouldHeadsUp(buildEntry {
                importance = NotificationManager.IMPORTANCE_HIGH
                isImportantConversation = true
            })
        }
    }

    @Test
    fun testAvalancheFilter_duringAvalanche_allowCall() {
        avalancheProvider.startTime = whenAgo(10)

        withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
            ensurePeekState()
            assertShouldHeadsUp(buildEntry {
                importance = NotificationManager.IMPORTANCE_HIGH
                isCall = true
            })
        }
    }

    @Test
    fun testAvalancheFilter_duringAvalanche_allowCategoryReminder() {
        avalancheProvider.startTime = whenAgo(10)

        withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
            ensurePeekState()
            assertShouldHeadsUp(buildEntry {
                importance = NotificationManager.IMPORTANCE_HIGH
                category = CATEGORY_REMINDER
            })
        }
    }

    @Test
    fun testAvalancheFilter_duringAvalanche_allowCategoryEvent() {
        avalancheProvider.startTime = whenAgo(10)

        withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
            ensurePeekState()
            assertShouldHeadsUp(buildEntry {
                importance = NotificationManager.IMPORTANCE_HIGH
                category = CATEGORY_EVENT
            })
        }
    }

    @Test
    fun testAvalancheFilter_duringAvalanche_allowFsi() {
        avalancheProvider.startTime = whenAgo(10)

        withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
            assertFsiNotSuppressed()
        }
    }

    @Test
    fun testAvalancheFilter_duringAvalanche_allowColorized() {
        avalancheProvider.startTime = whenAgo(10)

        withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) {
            ensurePeekState()
            assertShouldHeadsUp(buildEntry {
                importance = NotificationManager.IMPORTANCE_HIGH
                isColorized = true
            })
        }
    }

    @Test
    @Test
    fun testPeekCondition_suppressesOnlyPeek() {
    fun testPeekCondition_suppressesOnlyPeek() {
        withCondition(TestCondition(types = setOf(PEEK)) { true }) {
        withCondition(TestCondition(types = setOf(PEEK)) { true }) {
Loading