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

Commit f49f22f4 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Show one-time education HUN for suppression" into main

parents 2cc9ec96 b70bdab9
Loading
Loading
Loading
Loading
+72 −0
Original line number Diff line number Diff line
@@ -22,19 +22,25 @@ 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
import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.database.ContentObserver
import android.hardware.display.AmbientDisplayConfiguration
import android.os.Handler
import android.os.PowerManager
import android.os.SystemProperties
import android.provider.Settings
import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED
import android.provider.Settings.Global.HEADS_UP_OFF
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.settings.UserTracker
@@ -47,6 +53,7 @@ import com.android.systemui.statusbar.notification.interruption.VisualInterrupti
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.util.NotificationChannels
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SystemSettings
import com.android.systemui.util.time.SystemClock
@@ -244,12 +251,22 @@ class AlertKeyguardVisibilitySuppressor(
        keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
}

/**
 * Set with:
 * adb shell setprop persist.force_show_avalanche_edu_once 1 && adb shell stop; adb shell start
 */
private const val FORCE_SHOW_AVALANCHE_EDU_ONCE = "persist.force_show_avalanche_edu_once"

private const val PREF_HAS_SEEN_AVALANCHE_EDU = "has_seen_avalanche_edu"

class AvalancheSuppressor(
    private val avalancheProvider: AvalancheProvider,
    private val systemClock: SystemClock,
    private val systemSettings: SystemSettings,
    private val packageManager: PackageManager,
    private val uiEventLogger: UiEventLogger,
    private val context: Context,
    private val notificationManager: NotificationManager
) :
    VisualInterruptionFilter(
        types = setOf(PEEK, PULSE),
@@ -257,6 +274,24 @@ class AvalancheSuppressor(
    ) {
    val TAG = "AvalancheSuppressor"

    private val prefs = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)

    // SharedPreferences are persisted across reboots
    var hasSeenEdu: Boolean
        get() = prefs.getBoolean(PREF_HAS_SEEN_AVALANCHE_EDU, false)
        set(value) = prefs.edit().putBoolean(PREF_HAS_SEEN_AVALANCHE_EDU, value).apply()

    // Reset on reboot.
    // The pipeline runs these suppressors many times very fast, so we must use a separate bool
    // to force show for debug so that phone does not get stuck sending out infinite number of
    // education HUNs.
    private var hasShownOnceForDebug = false

    private fun shouldShowEdu() : Boolean {
        val forceShowOnce = SystemProperties.get(FORCE_SHOW_AVALANCHE_EDU_ONCE, "").equals("1")
        return !hasSeenEdu || (forceShowOnce && !hasShownOnceForDebug)
    }

    enum class State {
        ALLOW_CONVERSATION_AFTER_AVALANCHE,
        ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME,
@@ -309,9 +344,46 @@ class AvalancheSuppressor(
        if (state != State.SUPPRESS) {
            return false
        }
        if (shouldShowEdu()) {
            showEdu()
        }
        return true
    }

    /**
     * Show avalanche education HUN from SystemUI.
     */
    private fun showEdu() {
        val res = context.resources
        val titleStr = res.getString(
            com.android.systemui.res.R.string.adaptive_notification_edu_hun_title)
        val textStr = res.getString(
            com.android.systemui.res.R.string.adaptive_notification_edu_hun_text)
        val actionStr = res.getString(
            com.android.systemui.res.R.string.go_to_adaptive_notification_settings)

        val intent = Intent(Settings.ACTION_MANAGE_ADAPTIVE_NOTIFICATIONS)
        val pendingIntent = PendingIntent.getActivity(
            context, 0, intent,
            PendingIntent.FLAG_IMMUTABLE
        )

        val builder =
            Notification.Builder(context, NotificationChannels.ALERTS)
                .setTicker(titleStr)
                .setContentTitle(titleStr)
                .setContentText(textStr)
                .setSmallIcon(com.android.systemui.res.R.drawable.ic_settings)
                .setCategory(Notification.CATEGORY_SYSTEM)
                .setAutoCancel(true)
                .addAction(android.R.drawable.button_onoff_indicator_off, actionStr, pendingIntent)
                .setContentIntent(pendingIntent)

        notificationManager.notify(SystemMessage.NOTE_ADAPTIVE_NOTIFICATIONS, builder.build())
        hasSeenEdu = true
        hasShownOnceForDebug = true;
    }

    private fun calculateState(entry: NotificationEntry): State {
        if (
            entry.ranking.isConversation &&
+6 −2
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 */
package com.android.systemui.statusbar.notification.interruption

import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.display.AmbientDisplayConfiguration
import android.os.Handler
@@ -68,7 +70,9 @@ constructor(
    private val avalancheProvider: AvalancheProvider,
    private val systemSettings: SystemSettings,
    private val packageManager: PackageManager,
    private val bubbles: Optional<Bubbles>
    private val bubbles: Optional<Bubbles>,
    private val context: Context,
    private val notificationManager: NotificationManager
) : VisualInterruptionDecisionProvider {

    init {
@@ -179,7 +183,7 @@ constructor(
        if (NotificationAvalancheSuppression.isEnabled) {
            addFilter(
                AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                        uiEventLogger)
                        uiEventLogger, context, notificationManager)
            )
            avalancheProvider.register()
        }
+76 −16
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.Manifest.permission
import android.app.Notification.CATEGORY_EVENT
import android.app.Notification.CATEGORY_REMINDER
import android.app.NotificationManager
import android.content.pm.PackageManager.PERMISSION_DENIED
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -28,11 +29,16 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry
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 java.util.Optional
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.anyString
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.kotlin.whenever
import java.util.Optional

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -58,7 +64,9 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
            avalancheProvider,
            systemSettings,
            packageManager,
            Optional.of(bubbles)
            Optional.of(bubbles),
            context,
            notificationManager
        )
    }

@@ -86,13 +94,61 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
    // instead of VisualInterruptionDecisionProviderTestBase
    // because avalanche code is based on the suppression refactor.

    @Test
    fun testAvalancheFilter_suppress_hasNotSeenEdu_showEduHun() {
        setAllowedEmergencyPkg(false)
        whenever(avalancheProvider.timeoutMs).thenReturn(20)
        whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))

        val avalancheSuppressor = AvalancheSuppressor(
            avalancheProvider, systemClock, systemSettings, packageManager,
            uiEventLogger, context, notificationManager
        )
        avalancheSuppressor.hasSeenEdu = false

        withFilter(avalancheSuppressor) {
            ensurePeekState()
            assertShouldNotHeadsUp(
                buildEntry {
                    importance = NotificationManager.IMPORTANCE_HIGH
                    whenMs = whenAgo(5)
                }
            )
        }
        verify(notificationManager, times(1)).notify(anyInt(), any())
    }

    @Test
    fun testAvalancheFilter_suppress_hasSeenEduHun_doNotShowEduHun() {
        setAllowedEmergencyPkg(false)
        whenever(avalancheProvider.timeoutMs).thenReturn(20)
        whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))

        val avalancheSuppressor = AvalancheSuppressor(
            avalancheProvider, systemClock, systemSettings, packageManager,
            uiEventLogger, context, notificationManager
        )
        avalancheSuppressor.hasSeenEdu = true

        withFilter(avalancheSuppressor) {
            ensurePeekState()
            assertShouldNotHeadsUp(
                buildEntry {
                    importance = NotificationManager.IMPORTANCE_HIGH
                    whenMs = whenAgo(5)
                }
            )
        }
        verify(notificationManager, times(0)).notify(anyInt(), any())
    }

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

        withFilter(
            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                    uiEventLogger)
                    uiEventLogger, context, notificationManager)
        ) {
            ensurePeekState()
            assertShouldHeadsUp(
@@ -112,7 +168,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro

        withFilter(
            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                    uiEventLogger)
                    uiEventLogger, context, notificationManager)
        ) {
            ensurePeekState()
            assertShouldNotHeadsUp(
@@ -132,7 +188,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro

        withFilter(
            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                    uiEventLogger)
                    uiEventLogger, context, notificationManager)
        ) {
            ensurePeekState()
            assertShouldHeadsUp(
@@ -150,7 +206,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro

        withFilter(
            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                    uiEventLogger)
                    uiEventLogger, context, notificationManager)
        ) {
            ensurePeekState()
            assertShouldHeadsUp(
@@ -168,7 +224,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro

        withFilter(
            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                    uiEventLogger)
                    uiEventLogger, context, notificationManager)
        ) {
            ensurePeekState()
            assertShouldHeadsUp(
@@ -186,7 +242,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro

        withFilter(
            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                    uiEventLogger)
                    uiEventLogger, context, notificationManager)
        ) {
            ensurePeekState()
            assertShouldHeadsUp(
@@ -204,7 +260,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro

        withFilter(
            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                    uiEventLogger)
                    uiEventLogger, context, notificationManager)
        ) {
            assertFsiNotSuppressed()
        }
@@ -216,7 +272,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro

        withFilter(
            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                    uiEventLogger)
                    uiEventLogger, context, notificationManager)
        ) {
            ensurePeekState()
            assertShouldHeadsUp(
@@ -228,20 +284,24 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro
        }
    }

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

    private fun setAllowedEmergencyPkg(allow: Boolean) {
        `when`(
            packageManager.checkPermission(
                org.mockito.Mockito.eq(permission.RECEIVE_EMERGENCY_BROADCAST),
                anyString()
            )
        ).thenReturn(PERMISSION_GRANTED)
        ).thenReturn(if (allow) PERMISSION_GRANTED else PERMISSION_DENIED)
    }

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

        setAllowedEmergencyPkg(true)

        withFilter(
            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
                    uiEventLogger)
                    uiEventLogger, context, notificationManager)
        ) {
            ensurePeekState()
            assertShouldHeadsUp(
+2 −1
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.app.Notification.GROUP_ALERT_CHILDREN
import android.app.Notification.GROUP_ALERT_SUMMARY
import android.app.Notification.VISIBILITY_PRIVATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.NotificationManager.IMPORTANCE_LOW
@@ -133,7 +134,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() {
    protected val bubbles: Bubbles = mock()
    lateinit var systemSettings: SystemSettings
    protected val packageManager: PackageManager = mock()

    protected val notificationManager: NotificationManager = mock()
    protected abstract val provider: VisualInterruptionDecisionProvider

    private val neverSuppresses = object : NotificationInterruptSuppressor {}
+7 −1
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 */
package com.android.systemui.statusbar.notification.interruption

import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.display.AmbientDisplayConfiguration
import android.os.Handler
@@ -58,6 +60,8 @@ object VisualInterruptionDecisionProviderTestUtil {
        systemSettings: SystemSettings,
        packageManager: PackageManager,
        bubbles: Optional<Bubbles>,
        context: Context,
        notificationManager: NotificationManager
    ): VisualInterruptionDecisionProvider {
        return if (VisualInterruptionRefactor.isEnabled) {
            VisualInterruptionDecisionProviderImpl(
@@ -79,7 +83,9 @@ object VisualInterruptionDecisionProviderTestUtil {
                avalancheProvider,
                systemSettings,
                packageManager,
                bubbles
                bubbles,
                context,
                notificationManager
            )
        } else {
            NotificationInterruptStateProviderWrapper(
Loading