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

Commit 7a32c912 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Notifs] Create individual interactors for each status bar chip.

This CL defines a new `SingleNotificationChipInteractor` class. For each
promoted notification, we create a `SingleNotificationChipInteractor`
instance inside `StatusBarNotificationChipsInteractor`.  We keep track of
all those instances, and then the parent
`StatusBarNotificationChipsInteractor` accumulates the state across all
interactors and emits that accumulation as a list of actually-valid
chips.

This is pretty similar to how there's a top-level
MobileIconsInteractor to keep track of all the subscriptions, and then
individual `MobileIconInteractor` instances for each individual
subscription.

One reason to do this is that a future CL will hide a notification chip
if that notification's app is currently open, and that should happen on
an individual chip basis. That'll be much easier to implement if
`SingleNotificationChipInteractor` exists.

Bug: 364653005
Flag: com.android.systemui.status_bar_notification_chips
Test: Start a promoted ongoing notification -> verify chip appears.
Dismiss the promoted notification -> verify chip disappears
Test: atest SingleNotificationChipInteractorTest
StatusBarNotificationChipsInteractorTest

Change-Id: I3f47da63beaeae93b2b11c8ae1be516e2474cf11
parent 4d0fcaf7
Loading
Loading
Loading
Loading
+124 −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.chips.notification.domain.interactor

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.statusBarChipsLogger
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@SmallTest
@RunWith(AndroidJUnit4::class)
class SingleNotificationChipInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    val logger = kosmos.statusBarChipsLogger

    @Test
    fun notificationChip_startsWithStartingModel() =
        kosmos.runTest {
            val icon = mock<StatusBarIconView>()
            val startingNotif = activeNotificationModel(key = "notif1", statusBarChipIcon = icon)

            val underTest = SingleNotificationChipInteractor(startingNotif, logger)

            val latest by collectLastValue(underTest.notificationChip)

            assertThat(latest!!.key).isEqualTo("notif1")
            assertThat(latest!!.statusBarChipIconView).isEqualTo(icon)
        }

    @Test
    fun notificationChip_updatesAfterSet() =
        kosmos.runTest {
            val originalIconView = mock<StatusBarIconView>()
            val underTest =
                SingleNotificationChipInteractor(
                    activeNotificationModel(key = "notif1", statusBarChipIcon = originalIconView),
                    logger,
                )

            val latest by collectLastValue(underTest.notificationChip)

            val newIconView = mock<StatusBarIconView>()
            underTest.setNotification(
                activeNotificationModel(key = "notif1", statusBarChipIcon = newIconView)
            )

            assertThat(latest!!.key).isEqualTo("notif1")
            assertThat(latest!!.statusBarChipIconView).isEqualTo(newIconView)
        }

    @Test
    fun notificationChip_ignoresSetWithDifferentKey() =
        kosmos.runTest {
            val originalIconView = mock<StatusBarIconView>()
            val underTest =
                SingleNotificationChipInteractor(
                    activeNotificationModel(key = "notif1", statusBarChipIcon = originalIconView),
                    logger,
                )

            val latest by collectLastValue(underTest.notificationChip)

            val newIconView = mock<StatusBarIconView>()
            underTest.setNotification(
                activeNotificationModel(key = "other_notif", statusBarChipIcon = newIconView)
            )

            assertThat(latest!!.key).isEqualTo("notif1")
            assertThat(latest!!.statusBarChipIconView).isEqualTo(originalIconView)
        }

    @Test
    fun notificationChip_missingStatusBarIconChipView_inConstructor_emitsNull() =
        kosmos.runTest {
            val underTest =
                SingleNotificationChipInteractor(
                    activeNotificationModel(key = "notif1", statusBarChipIcon = null),
                    logger,
                )

            val latest by collectLastValue(underTest.notificationChip)

            assertThat(latest).isNull()
        }

    @Test
    fun notificationChip_missingStatusBarIconChipView_inSet_emitsNull() =
        kosmos.runTest {
            val startingNotif = activeNotificationModel(key = "notif1", statusBarChipIcon = mock())
            val underTest = SingleNotificationChipInteractor(startingNotif, logger)
            val latest by collectLastValue(underTest.notificationChip)
            assertThat(latest).isNotNull()

            underTest.setNotification(
                activeNotificationModel(key = "notif1", statusBarChipIcon = null)
            )

            assertThat(latest).isNull()
        }
}
+257 −2
Original line number Diff line number Diff line
@@ -16,30 +16,277 @@

package com.android.systemui.statusbar.chips.notification.domain.interactor

import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val testScope = kosmos.testScope
    private val activeNotificationListRepository = kosmos.activeNotificationListRepository

    private val underTest by lazy {
        kosmos.statusBarNotificationChipsInteractor.also { it.start() }
    }

    @Test
    @DisableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_flagOff_noNotifs() =
        testScope.runTest {
            val latest by collectLastValue(underTest.notificationChips)

            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = mock<StatusBarIconView>(),
                        isPromoted = true,
                    )
                )
            )

            assertThat(latest).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_noNotifs_empty() =
        testScope.runTest {
            val latest by collectLastValue(underTest.notificationChips)

            setNotifs(emptyList())

            assertThat(latest).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_notifMissingStatusBarChipIconView_empty() =
        testScope.runTest {
            val latest by collectLastValue(underTest.notificationChips)

            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = null,
                        isPromoted = true,
                    )
                )
            )

            assertThat(latest).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_onePromotedNotif_statusBarIconViewMatches() =
        testScope.runTest {
            val latest by collectLastValue(underTest.notificationChips)

            val icon = mock<StatusBarIconView>()
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = icon,
                        isPromoted = true,
                    )
                )
            )

            assertThat(latest).hasSize(1)
            assertThat(latest!![0].key).isEqualTo("notif")
            assertThat(latest!![0].statusBarChipIconView).isEqualTo(icon)
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_onlyForPromotedNotifs() =
        testScope.runTest {
            val latest by collectLastValue(underTest.notificationChips)

            val firstIcon = mock<StatusBarIconView>()
            val secondIcon = mock<StatusBarIconView>()
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif1",
                        statusBarChipIcon = firstIcon,
                        isPromoted = true,
                    ),
                    activeNotificationModel(
                        key = "notif2",
                        statusBarChipIcon = secondIcon,
                        isPromoted = true,
                    ),
                    activeNotificationModel(
                        key = "notif3",
                        statusBarChipIcon = mock<StatusBarIconView>(),
                        isPromoted = false,
                    ),
                )
            )

            assertThat(latest).hasSize(2)
            assertThat(latest!![0].key).isEqualTo("notif1")
            assertThat(latest!![0].statusBarChipIconView).isEqualTo(firstIcon)
            assertThat(latest!![1].key).isEqualTo("notif2")
            assertThat(latest!![1].statusBarChipIconView).isEqualTo(secondIcon)
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_notifUpdatesGoThrough() =
        testScope.runTest {
            val latest by collectLastValue(underTest.notificationChips)

            val firstIcon = mock<StatusBarIconView>()
            val secondIcon = mock<StatusBarIconView>()
            val thirdIcon = mock<StatusBarIconView>()

    private val underTest = kosmos.statusBarNotificationChipsInteractor
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = firstIcon,
                        isPromoted = true,
                    )
                )
            )
            assertThat(latest).hasSize(1)
            assertThat(latest!![0].key).isEqualTo("notif")
            assertThat(latest!![0].statusBarChipIconView).isEqualTo(firstIcon)

            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = secondIcon,
                        isPromoted = true,
                    )
                )
            )
            assertThat(latest).hasSize(1)
            assertThat(latest!![0].key).isEqualTo("notif")
            assertThat(latest!![0].statusBarChipIconView).isEqualTo(secondIcon)

            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = thirdIcon,
                        isPromoted = true,
                    )
                )
            )
            assertThat(latest).hasSize(1)
            assertThat(latest!![0].key).isEqualTo("notif")
            assertThat(latest!![0].statusBarChipIconView).isEqualTo(thirdIcon)
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_promotedNotifDisappearsThenReappears() =
        testScope.runTest {
            val latest by collectLastValue(underTest.notificationChips)

            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = mock(),
                        isPromoted = true,
                    )
                )
            )
            assertThat(latest).hasSize(1)
            assertThat(latest!![0].key).isEqualTo("notif")

            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = mock(),
                        isPromoted = false,
                    )
                )
            )
            assertThat(latest).isEmpty()

            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = mock(),
                        isPromoted = true,
                    )
                )
            )
            assertThat(latest).hasSize(1)
            assertThat(latest!![0].key).isEqualTo("notif")
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_notifChangesKey() =
        testScope.runTest {
            val latest by collectLastValue(underTest.notificationChips)

            val firstIcon = mock<StatusBarIconView>()
            val secondIcon = mock<StatusBarIconView>()
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif|uid1",
                        statusBarChipIcon = firstIcon,
                        isPromoted = true,
                    )
                )
            )
            assertThat(latest).hasSize(1)
            assertThat(latest!![0].key).isEqualTo("notif|uid1")
            assertThat(latest!![0].statusBarChipIconView).isEqualTo(firstIcon)

            // WHEN a notification changes UID, which is a key change
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif|uid2",
                        statusBarChipIcon = secondIcon,
                        isPromoted = true,
                    )
                )
            )

            // THEN we correctly update
            assertThat(latest).hasSize(1)
            assertThat(latest!![0].key).isEqualTo("notif|uid2")
            assertThat(latest!![0].statusBarChipIconView).isEqualTo(secondIcon)
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun onPromotedNotificationChipTapped_emitsKeys() =
        testScope.runTest {
            val latest by collectValues(underTest.promotedNotificationChipTapEvent)
@@ -56,6 +303,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun onPromotedNotificationChipTapped_sameKeyTwice_emitsTwice() =
        testScope.runTest {
            val latest by collectValues(underTest.promotedNotificationChipTapEvent)
@@ -67,4 +315,11 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
            assertThat(latest[0]).isEqualTo("fakeKey")
            assertThat(latest[1]).isEqualTo("fakeKey")
        }

    private fun setNotifs(notifs: List<ActiveNotificationModel>) {
        activeNotificationListRepository.activeNotifications.value =
            ActiveNotificationsStore.Builder()
                .apply { notifs.forEach { addIndividualNotif(it) } }
                .build()
    }
}
+16 −13
Original line number Diff line number Diff line
@@ -22,7 +22,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
@@ -34,26 +36,28 @@ import com.android.systemui.statusbar.notification.shared.ActiveNotificationMode
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
class NotifChipsViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val activeNotificationListRepository = kosmos.activeNotificationListRepository

    private val underTest = kosmos.notifChipsViewModel
    private val underTest by lazy { kosmos.notifChipsViewModel }

    @Before
    fun setUp() {
        kosmos.statusBarNotificationChipsInteractor.start()
    }

    @Test
    fun chips_noNotifs_empty() =
        testScope.runTest {
        kosmos.runTest {
            val latest by collectLastValue(underTest.chips)

            setNotifs(emptyList())
@@ -63,7 +67,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {

    @Test
    fun chips_notifMissingStatusBarChipIconView_empty() =
        testScope.runTest {
        kosmos.runTest {
            val latest by collectLastValue(underTest.chips)

            setNotifs(
@@ -81,7 +85,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {

    @Test
    fun chips_onePromotedNotif_statusBarIconViewMatches() =
        testScope.runTest {
        kosmos.runTest {
            val latest by collectLastValue(underTest.chips)

            val icon = mock<StatusBarIconView>()
@@ -103,7 +107,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {

    @Test
    fun chips_onlyForPromotedNotifs() =
        testScope.runTest {
        kosmos.runTest {
            val latest by collectLastValue(underTest.chips)

            val firstIcon = mock<StatusBarIconView>()
@@ -135,7 +139,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {

    @Test
    fun chips_clickingChipNotifiesInteractor() =
        testScope.runTest {
        kosmos.runTest {
            val latest by collectLastValue(underTest.chips)
            val latestChipTap by
                collectLastValue(
@@ -163,7 +167,6 @@ class NotifChipsViewModelTest : SysuiTestCase() {
            ActiveNotificationsStore.Builder()
                .apply { notifs.forEach { addIndividualNotif(it) } }
                .build()
        testScope.runCurrent()
    }

    companion object {
+7 −2
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
@@ -38,6 +39,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.DemoNotifChipViewModelTest.Companion.addDemoNotifChip
import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.demoNotifChipViewModel
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModelTest.Companion.assertIsNotifChip
import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
@@ -67,6 +69,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -79,7 +82,7 @@ import org.mockito.kotlin.whenever
@OptIn(ExperimentalCoroutinesApi::class)
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val testScope = kosmos.testScope
    private val systemClock = kosmos.fakeSystemClock
    private val commandRegistry = kosmos.commandRegistry
@@ -103,12 +106,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
                .thenReturn(chipBackgroundView)
        }

    private val underTest = kosmos.ongoingActivityChipsViewModel
    private val underTest by lazy { kosmos.ongoingActivityChipsViewModel }

    @Before
    fun setUp() {
        setUpPackageManagerForMediaProjection(kosmos)
        kosmos.demoNotifChipViewModel.start()
        kosmos.statusBarNotificationChipsInteractor.start()
        val icon =
            BitmapDrawable(
                context.resources,
@@ -616,6 +620,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
        }

    @Test
    @Ignore("b/364653005") // We'll need to re-do the animation story when we implement RON chips
    fun primaryChip_screenRecordStoppedViaDialog_chipHiddenWithoutAnimation() =
        testScope.runTest {
            screenRecordState.value = ScreenRecordModel.Recording
+3 −1
Original line number Diff line number Diff line
@@ -84,7 +84,9 @@ import org.mockito.MockitoAnnotations
class HeadsUpCoordinatorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val statusBarNotificationChipsInteractor = kosmos.statusBarNotificationChipsInteractor
    private val statusBarNotificationChipsInteractor by lazy {
        kosmos.statusBarNotificationChipsInteractor
    }
    private val notifCollection = kosmos.mockNotifCollection

    private lateinit var coordinator: HeadsUpCoordinator
Loading