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

Commit 6fc4d0c9 authored by Lyn's avatar Lyn
Browse files

Suppress HUNs after avalanche, with allowlist

VisualInterruptionDecisionProviderImpl:
    Add BroadcastDispatcher to register broadcast receiver
    that updates avalanche info in AvalancheProvider

CommonVisualInterruptionSuppressors:
    Add AvalancheSuppressor

Fixes: 322060786
Test: VisualInterruptionDecisionProviderImplTest
Test: enable flags

adb shell device_config override systemui com.android.systemui.visual_interruptions_refactor true

adb shell device_config override systemui com.android.systemui.notification_avalanche_suppression true

Allowlist:
Conversation after avalanche time
High priority conversation from any time
CallStyles
CATEGORY_REMINDER
CATEGORY_EVENT
FSI with permission on
Colorized

HUNs not in the allowlist are suppressed for two minutes after:
Turning phone ON
Turning airplane mode OFF
Turning work profile ON
Switching user

Flag: ACONFIG notification_avalanche_suppression DEVELOPMENT
Change-Id: I86314c3df4e8d079bffbe8489a5474a1e984dc68
parent 6561d91e
Loading
Loading
Loading
Loading
+71 −0
Original line number 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 Diff line number Diff line
@@ -16,7 +16,10 @@

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

import android.app.Notification
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.NotificationManager.IMPORTANCE_DEFAULT
import android.app.NotificationManager.IMPORTANCE_HIGH
@@ -224,3 +227,68 @@ class AlertKeyguardVisibilitySuppressor(
    override fun shouldSuppress(entry: NotificationEntry) =
        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 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.PEEK
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.DeviceProvisionedController
import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -60,7 +61,10 @@ constructor(
        private val systemClock: SystemClock,
        private val uiEventLogger: UiEventLogger,
        private val userTracker: UserTracker,
        private val avalancheProvider: AvalancheProvider

) : VisualInterruptionDecisionProvider {

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

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

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

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.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
@@ -47,6 +50,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            systemClock,
            uiEventLogger,
            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
    fun testPeekCondition_suppressesOnlyPeek() {
        withCondition(TestCondition(types = setOf(PEEK)) { true }) {
Loading