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

Commit 87be0dff authored by Julia Tuttle's avatar Julia Tuttle
Browse files

Improve visual interruption decision provider tests

Also, fix a couple related issues.

Bug: 261728888
Test: atest VisualInterruptionDecisionProviderImplTest
Flag: ACONFIG com.android.systemui.visual_interruptions_refactor DEVELOPMENT
Change-Id: I72bfdfcf521022ac89a6527ac48ab50505f11bb2
parent 14a03f24
Loading
Loading
Loading
Loading
+22 −22
Original line number Diff line number Diff line
@@ -35,8 +35,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
class NotificationInterruptStateProviderWrapperTest : VisualInterruptionDecisionProviderTestBase() {
    override val provider: VisualInterruptionDecisionProvider
        get() =
    override val provider by lazy {
        NotificationInterruptStateProviderWrapper(
            NotificationInterruptStateProviderImpl(
                    powerManager,
@@ -57,6 +56,7 @@ class NotificationInterruptStateProviderWrapperTest : VisualInterruptionDecision
                )
                .also { it.mUseHeadsUp = true }
        )
    }

    // Tests of internals of the wrapper:

+292 −94
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.NotificationManager.VISIBILITY_NO_OVERRIDE
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_MUTABLE
import android.content.Context
import android.content.Intent
import android.content.pm.UserInfo
import android.graphics.drawable.Icon
@@ -53,6 +54,7 @@ import com.android.systemui.util.settings.FakeGlobalSettings
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.utils.leaks.FakeBatteryController
import com.android.systemui.utils.leaks.LeakCheckedTest
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -80,6 +82,23 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() {

    protected abstract val provider: VisualInterruptionDecisionProvider

    private val neverSuppresses = object : NotificationInterruptSuppressor {}

    private val alwaysSuppressesInterruptions =
        object : NotificationInterruptSuppressor {
            override fun suppressInterruptions(entry: NotificationEntry?) = true
        }

    private val alwaysSuppressesAwakeInterruptions =
        object : NotificationInterruptSuppressor {
            override fun suppressAwakeInterruptions(entry: NotificationEntry?) = true
        }

    private val alwaysSuppressesAwakeHeadsUp =
        object : NotificationInterruptSuppressor {
            override fun suppressAwakeHeadsUp(entry: NotificationEntry?) = true
        }

    @Before
    fun setUp() {
        globalSettings.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON)
@@ -87,154 +106,333 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() {
        val user = UserInfo(ActivityManager.getCurrentUser(), "Current user", /* flags = */ 0)
        userTracker.set(listOf(user), /* currentUserIndex = */ 0)

        whenever(headsUpManager.isSnoozed(any())).thenReturn(false)
        whenever(keyguardNotificationVisibilityProvider.shouldHideNotification(any()))
            .thenReturn(false)
    }

    @Test
    fun testShouldPeek() {
        ensureStateForPeek()
        ensurePeekState()
        assertShouldHeadsUp(buildPeekEntry())
    }

        assertTrue(provider.makeUnloggedHeadsUpDecision(createPeekEntry()).shouldInterrupt)
    @Test
    fun testShouldPeek_defaultLegacySuppressor() {
        ensurePeekState()
        provider.addLegacySuppressor(neverSuppresses)
        assertShouldHeadsUp(buildPeekEntry())
    }

    @Test
    fun testShouldPulse() {
        ensureStateForPulse()
    fun testShouldNotPeek_legacySuppressInterruptions() {
        ensurePeekState()
        provider.addLegacySuppressor(alwaysSuppressesInterruptions)
        assertShouldNotHeadsUp(buildPeekEntry())
    }

        assertTrue(provider.makeUnloggedHeadsUpDecision(createPulseEntry()).shouldInterrupt)
    @Test
    fun testShouldNotPeek_legacySuppressAwakeInterruptions() {
        ensurePeekState()
        provider.addLegacySuppressor(alwaysSuppressesAwakeInterruptions)
        assertShouldNotHeadsUp(buildPeekEntry())
    }

    @Test
    fun testShouldFsi_awake() {
        ensureStateForAwakeFsi()
    fun testShouldNotPeek_legacySuppressAwakeHeadsUp() {
        ensurePeekState()
        provider.addLegacySuppressor(alwaysSuppressesAwakeHeadsUp)
        assertShouldNotHeadsUp(buildPeekEntry())
    }

        assertTrue(provider.makeUnloggedFullScreenIntentDecision(createFsiEntry()).shouldInterrupt)
    @Test
    fun testShouldPulse() {
        ensurePulseState()
        assertShouldHeadsUp(buildPulseEntry())
    }

    @Test
    fun testShouldFsi_dreaming() {
        ensureStateForDreamingFsi()
    fun testShouldPulse_defaultLegacySuppressor() {
        ensurePulseState()
        provider.addLegacySuppressor(neverSuppresses)
        assertShouldHeadsUp(buildPulseEntry())
    }

        assertTrue(provider.makeUnloggedFullScreenIntentDecision(createFsiEntry()).shouldInterrupt)
    @Test
    fun testShouldNotPulse_legacySuppressInterruptions() {
        ensurePulseState()
        provider.addLegacySuppressor(alwaysSuppressesInterruptions)
        assertShouldNotHeadsUp(buildPulseEntry())
    }

    @Test
    fun testShouldFsi_keyguard() {
        ensureStateForKeyguardFsi()
    fun testShouldPulse_legacySuppressAwakeInterruptions() {
        ensurePulseState()
        provider.addLegacySuppressor(alwaysSuppressesAwakeInterruptions)
        assertShouldHeadsUp(buildPulseEntry())
    }

        assertTrue(provider.makeUnloggedFullScreenIntentDecision(createFsiEntry()).shouldInterrupt)
    @Test
    fun testShouldPulse_legacySuppressAwakeHeadsUp() {
        ensurePulseState()
        provider.addLegacySuppressor(alwaysSuppressesAwakeHeadsUp)
        assertShouldHeadsUp(buildPulseEntry())
    }

    @Test
    fun testShouldBubble() {
        assertTrue(provider.makeAndLogBubbleDecision(createBubbleEntry()).shouldInterrupt)
        ensureBubbleState()
        assertShouldBubble(buildBubbleEntry())
    }

    @Test
    fun testShouldBubble_defaultLegacySuppressor() {
        ensureBubbleState()
        provider.addLegacySuppressor(neverSuppresses)
        assertShouldBubble(buildBubbleEntry())
    }

    @Test
    fun testShouldNotBubble_legacySuppressInterruptions() {
        ensureBubbleState()
        provider.addLegacySuppressor(alwaysSuppressesInterruptions)
        assertShouldNotBubble(buildBubbleEntry())
    }

    @Test
    fun testShouldNotBubble_legacySuppressAwakeInterruptions() {
        ensureBubbleState()
        provider.addLegacySuppressor(alwaysSuppressesAwakeInterruptions)
        assertShouldNotBubble(buildBubbleEntry())
    }

    private fun ensureStateForPeek() {
        whenever(powerManager.isScreenOn).thenReturn(true)
        statusBarStateController.dozing = false
        statusBarStateController.dreaming = false
    @Test
    fun testShouldBubble_legacySuppressAwakeHeadsUp() {
        ensureBubbleState()
        provider.addLegacySuppressor(alwaysSuppressesAwakeHeadsUp)
        assertShouldBubble(buildBubbleEntry())
    }

    private fun ensureStateForPulse() {
        ambientDisplayConfiguration.fakePulseOnNotificationEnabled = true
        batteryController.setIsAodPowerSave(false)
        statusBarStateController.dozing = true
    @Test
    fun testShouldFsi_notInteractive() {
        ensureNotInteractiveFsiState()
        assertShouldFsi(buildFsiEntry())
    }

    private fun ensureStateForAwakeFsi() {
        whenever(powerManager.isInteractive).thenReturn(false)
        statusBarStateController.dreaming = false
        statusBarStateController.state = SHADE
    @Test
    fun testShouldFsi_dreaming() {
        ensureDreamingFsiState()
        assertShouldFsi(buildFsiEntry())
    }

    @Test
    fun testShouldFsi_keyguard() {
        ensureKeyguardFsiState()
        assertShouldFsi(buildFsiEntry())
    }

    private data class State(
        var hunSnoozed: Boolean? = null,
        var isAodPowerSave: Boolean? = null,
        var isDozing: Boolean? = null,
        var isDreaming: Boolean? = null,
        var isInteractive: Boolean? = null,
        var isScreenOn: Boolean? = null,
        var keyguardShouldHideNotification: Boolean? = null,
        var pulseOnNotificationsEnabled: Boolean? = null,
        var statusBarState: Int? = null,
    )

    private fun setState(state: State): Unit =
        state.run {
            hunSnoozed?.let { whenever(headsUpManager.isSnoozed(TEST_PACKAGE)).thenReturn(it) }

            isAodPowerSave?.let { batteryController.setIsAodPowerSave(it) }

            isDozing?.let { statusBarStateController.dozing = it }

            isDreaming?.let { statusBarStateController.dreaming = it }

            isInteractive?.let { whenever(powerManager.isInteractive).thenReturn(it) }

            isScreenOn?.let { whenever(powerManager.isScreenOn).thenReturn(it) }

            keyguardShouldHideNotification?.let {
                whenever(keyguardNotificationVisibilityProvider.shouldHideNotification(any()))
                    .thenReturn(it)
            }

    private fun ensureStateForDreamingFsi() {
        whenever(powerManager.isInteractive).thenReturn(true)
        statusBarStateController.dreaming = true
        statusBarStateController.state = SHADE
            pulseOnNotificationsEnabled?.let {
                ambientDisplayConfiguration.fakePulseOnNotificationEnabled = it
            }

    private fun ensureStateForKeyguardFsi() {
        whenever(powerManager.isInteractive).thenReturn(true)
        statusBarStateController.dreaming = false
        statusBarStateController.state = KEYGUARD
            statusBarState?.let { statusBarStateController.state = it }
        }

    private fun createNotif(
        hasFsi: Boolean = false,
        bubbleMetadata: BubbleMetadata? = null
    ): Notification {
        return Notification.Builder(context, TEST_CHANNEL_ID)
    private fun ensureState(block: State.() -> Unit) =
        State()
            .apply {
                setContentTitle(TEST_CONTENT_TITLE)
                setContentText(TEST_CONTENT_TEXT)
                keyguardShouldHideNotification = false
                apply(block)
            }
            .run(this::setState)

    private fun ensurePeekState(block: State.() -> Unit = {}) = ensureState {
        hunSnoozed = false
        isDozing = false
        isDreaming = false
        isScreenOn = true
        run(block)
    }

                if (hasFsi) {
                    setFullScreenIntent(mock(), /* highPriority = */ true)
    private fun ensurePulseState(block: State.() -> Unit = {}) = ensureState {
        isAodPowerSave = false
        isDozing = true
        pulseOnNotificationsEnabled = true
        run(block)
    }

                if (bubbleMetadata != null) {
                    setBubbleMetadata(bubbleMetadata)
    private fun ensureBubbleState(block: State.() -> Unit = {}) = ensureState(block)

    private fun ensureNotInteractiveFsiState(block: State.() -> Unit = {}) = ensureState {
        isDreaming = false
        isInteractive = false
        statusBarState = SHADE
        run(block)
    }

    private fun ensureDreamingFsiState(block: State.() -> Unit = {}) = ensureState {
        isDreaming = true
        isInteractive = true
        statusBarState = SHADE
        run(block)
    }
            .setContentTitle(TEST_CONTENT_TITLE)
            .setContentText(TEST_CONTENT_TEXT)
            .build()

    private fun ensureKeyguardFsiState(block: State.() -> Unit = {}) = ensureState {
        isDreaming = false
        isInteractive = true
        statusBarState = KEYGUARD
        run(block)
    }

    private fun assertShouldHeadsUp(entry: NotificationEntry) =
        provider.makeUnloggedHeadsUpDecision(entry).let {
            assertTrue("unexpected suppressed HUN: ${it.logReason}", it.shouldInterrupt)
        }

    private fun assertShouldNotHeadsUp(entry: NotificationEntry) =
        provider.makeUnloggedHeadsUpDecision(entry).let {
            assertFalse("unexpected unsuppressed HUN: ${it.logReason}", it.shouldInterrupt)
        }

    private fun assertShouldBubble(entry: NotificationEntry) =
        provider.makeAndLogBubbleDecision(entry).let {
            assertTrue("unexpected suppressed bubble: ${it.logReason}", it.shouldInterrupt)
        }

    private fun assertShouldNotBubble(entry: NotificationEntry) =
        provider.makeAndLogBubbleDecision(entry).let {
            assertFalse("unexpected unsuppressed bubble: ${it.logReason}", it.shouldInterrupt)
        }

    private fun assertShouldFsi(entry: NotificationEntry) =
        provider.makeUnloggedFullScreenIntentDecision(entry).let {
            assertTrue("unexpected suppressed FSI: ${it.logReason}", it.shouldInterrupt)
        }

    private fun createBubbleMetadata(): BubbleMetadata {
        val pendingIntent =
    private fun assertShouldNotFsi(entry: NotificationEntry) =
        provider.makeUnloggedFullScreenIntentDecision(entry).let {
            assertFalse("unexpected unsuppressed FSI: ${it.logReason}", it.shouldInterrupt)
        }

    private class EntryBuilder(val context: Context) {
        var importance = IMPORTANCE_DEFAULT
        var suppressedVisualEffects: Int? = null
        var whenMs: Long? = null
        var visibilityOverride: Int? = null
        var hasFsi = false
        var canBubble: Boolean? = null
        var hasBubbleMetadata = false
        var bubbleSuppressNotification: Boolean? = null

        private fun buildBubbleMetadata() =
            BubbleMetadata.Builder(
                    PendingIntent.getActivity(
                        context,
                        /* requestCode = */ 0,
                        Intent().setPackage(context.packageName),
                        FLAG_MUTABLE
                    ),
                    Icon.createWithResource(context.resources, R.drawable.android)
                )
                .apply { bubbleSuppressNotification?.let { setSuppressNotification(it) } }
                .build()

        val icon = Icon.createWithResource(context.resources, R.drawable.android)
        fun build() =
            Notification.Builder(context, TEST_CHANNEL_ID)
                .apply {
                    setContentTitle(TEST_CONTENT_TITLE)
                    setContentText(TEST_CONTENT_TEXT)

        return BubbleMetadata.Builder(pendingIntent, icon).build()
                    if (hasFsi) {
                        setFullScreenIntent(mock(), /* highPriority = */ true)
                    }

    private fun createEntry(
        notif: Notification,
        importance: Int = IMPORTANCE_DEFAULT,
        canBubble: Boolean? = null
    ): NotificationEntry {
        return NotificationEntryBuilder()
                    whenMs?.let { setWhen(it) }

                    if (hasBubbleMetadata) {
                        setBubbleMetadata(buildBubbleMetadata())
                    }
                }
                .build()
                .let { NotificationEntryBuilder().setNotification(it) }
                .apply {
                    setPkg(TEST_PACKAGE)
                    setOpPkg(TEST_PACKAGE)
                    setTag(TEST_TAG)
                setChannel(NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_NAME, importance))
                setNotification(notif)

                    setImportance(importance)
                    setChannel(NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_NAME, importance))

                if (canBubble != null) {
                    setCanBubble(canBubble)
                    canBubble?.let { setCanBubble(it) }
                }
                .build()!!
                .also {
                    modifyRanking(it)
                        .apply {
                            suppressedVisualEffects?.let { setSuppressedVisualEffects(it) }
                            visibilityOverride?.let { setVisibilityOverride(it) }
                        }
                        .build()
                }
    }

    private fun createPeekEntry() = createEntry(notif = createNotif(), importance = IMPORTANCE_HIGH)
    private fun buildEntry(block: EntryBuilder.() -> Unit) =
        EntryBuilder(context).also(block).build()

    private fun createPulseEntry() =
        createEntry(notif = createNotif(), importance = IMPORTANCE_HIGH).also {
            modifyRanking(it).setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build()
    private fun buildPeekEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry {
        importance = IMPORTANCE_HIGH
        run(block)
    }

    private fun createFsiEntry() =
        createEntry(notif = createNotif(hasFsi = true), importance = IMPORTANCE_HIGH)
    private fun buildPulseEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry {
        importance = IMPORTANCE_DEFAULT
        visibilityOverride = VISIBILITY_NO_OVERRIDE
        run(block)
    }

    private fun createBubbleEntry() =
        createEntry(
            notif = createNotif(bubbleMetadata = createBubbleMetadata()),
            importance = IMPORTANCE_HIGH,
    private fun buildFsiEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry {
        importance = IMPORTANCE_HIGH
        hasFsi = true
        run(block)
    }

    private fun buildBubbleEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry {
        canBubble = true
        )
        hasBubbleMetadata = true
        run(block)
    }

    private fun whenAgo(whenAgeMs: Long) = systemClock.currentTimeMillis() - whenAgeMs
}

private const val TEST_CONTENT_TITLE = "Test Content Title"
+1 −1
Original line number Diff line number Diff line
@@ -104,7 +104,7 @@ class FakeStatusBarStateController : SysuiStatusBarStateController {

    override fun isDreaming() = dreaming

    override fun setIsDreaming(drreaming: Boolean): Boolean {
    override fun setIsDreaming(dreaming: Boolean): Boolean {
        dreaming != this.dreaming || return false
        this.dreaming = dreaming
        callbacks.forEach { it.onDreamingChanged(dreaming) }