summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt124
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt259
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt29
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt92
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt103
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt19
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt7
12 files changed, 668 insertions, 40 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
new file mode 100644
index 000000000000..8f210098a727
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
index 19ed6a57d2f0..702e101d2d39 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
@@ -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 = kosmos.statusBarNotificationChipsInteractor
+ 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>()
+
+ 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()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 1b4132910555..16376c5b3850 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -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 {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 25d5ce50e03f..eb0978eff24b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -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
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 2c488e3a7242..86a51912cc4f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -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
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt
index 8ce0dbf8e171..6db610bbc3a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt
@@ -21,7 +21,10 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
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 dagger.Binds
+import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
@@ -41,5 +44,19 @@ abstract class StatusBarChipsModule {
fun provideChipsLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create("StatusBarChips", 200)
}
+
+ @Provides
+ @SysUISingleton
+ @IntoMap
+ @ClassKey(StatusBarNotificationChipsInteractor::class)
+ fun statusBarNotificationChipsInteractorAsCoreStartable(
+ interactorLazy: Lazy<StatusBarNotificationChipsInteractor>
+ ): CoreStartable {
+ return if (StatusBarNotifChips.isEnabled) {
+ interactorLazy.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
new file mode 100644
index 000000000000..b96359d4f33f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
@@ -0,0 +1,92 @@
+/*
+ * 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 com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad
+import com.android.systemui.statusbar.chips.StatusBarChipsLog
+import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Interactor representing a single notification's status bar chip.
+ *
+ * [startingModel.key] dictates which notification this interactor corresponds to - all updates sent
+ * to this interactor via [setNotification] should only be for the notification with the same key.
+ *
+ * [StatusBarNotificationChipsInteractor] will collect all the individual instances of this
+ * interactor and send all the necessary information to the UI layer.
+ */
+class SingleNotificationChipInteractor
+@AssistedInject
+constructor(
+ @Assisted startingModel: ActiveNotificationModel,
+ @StatusBarChipsLog private val logBuffer: LogBuffer,
+) {
+ private val key = startingModel.key
+ private val logger = Logger(logBuffer, "Notif".pad())
+ // [StatusBarChipLogTag] recommends a max tag length of 20, so [extraLogTag] should NOT be the
+ // top-level tag. It should instead be provided as the first string in each log message.
+ private val extraLogTag = "SingleChipInteractor[key=$key]"
+
+ private val _notificationModel = MutableStateFlow(startingModel)
+
+ /**
+ * Sets the new notification info corresponding to this interactor. The key on [model] *must*
+ * match the key on the original [startingModel], otherwise the update won't be processed.
+ */
+ fun setNotification(model: ActiveNotificationModel) {
+ if (model.key != this.key) {
+ logger.w({ "$str1: received model for different key $str2" }) {
+ str1 = extraLogTag
+ str2 = model.key
+ }
+ return
+ }
+ _notificationModel.value = model
+ }
+
+ /**
+ * Emits this notification's status bar chip, or null if this notification shouldn't show a
+ * status bar chip.
+ */
+ val notificationChip: Flow<NotificationChipModel?> =
+ _notificationModel.map { notif -> notif.toNotificationChipModel() }
+
+ private fun ActiveNotificationModel.toNotificationChipModel(): NotificationChipModel? {
+ val statusBarChipIconView = this.statusBarChipIconView
+ if (statusBarChipIconView == null) {
+ logger.w({ "$str1: Can't show chip because status bar chip icon view is null" }) {
+ str1 = extraLogTag
+ }
+ return null
+ }
+ return NotificationChipModel(key, statusBarChipIconView)
+ }
+
+ @AssistedFactory
+ fun interface Factory {
+ fun create(startingModel: ActiveNotificationModel): SingleNotificationChipInteractor
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
index 9e09671bc7bf..e8cb35b06999 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt
@@ -17,16 +17,42 @@
package com.android.systemui.statusbar.chips.notification.domain.interactor
import android.annotation.SuppressLint
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad
+import com.android.systemui.statusbar.chips.StatusBarChipsLog
+import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
/** An interactor for the notification chips shown in the status bar. */
@SysUISingleton
-class StatusBarNotificationChipsInteractor @Inject constructor() {
+@OptIn(ExperimentalCoroutinesApi::class)
+class StatusBarNotificationChipsInteractor
+@Inject
+constructor(
+ @Background private val backgroundScope: CoroutineScope,
+ private val activeNotificationsInteractor: ActiveNotificationsInteractor,
+ private val singleNotificationChipInteractorFactory: SingleNotificationChipInteractor.Factory,
+ @StatusBarChipsLog private val logBuffer: LogBuffer,
+) : CoreStartable {
+ private val logger = Logger(logBuffer, "AllNotifs".pad())
// Each chip tap is an individual event, *not* a state, which is why we're using SharedFlow not
// StateFlow. There shouldn't be multiple updates per frame, which should avoid performance
@@ -45,4 +71,79 @@ class StatusBarNotificationChipsInteractor @Inject constructor() {
StatusBarNotifChips.assertInNewMode()
_promotedNotificationChipTapEvent.emit(key)
}
+
+ /**
+ * A cache of interactors. Each currently-promoted notification should have a corresponding
+ * interactor in this map.
+ */
+ private val promotedNotificationInteractorMap =
+ mutableMapOf<String, SingleNotificationChipInteractor>()
+
+ /**
+ * A list of interactors. Each currently-promoted notification should have a corresponding
+ * interactor in this list.
+ */
+ private val promotedNotificationInteractors =
+ MutableStateFlow<List<SingleNotificationChipInteractor>>(emptyList())
+
+ override fun start() {
+ if (!StatusBarNotifChips.isEnabled) {
+ return
+ }
+
+ backgroundScope.launch("StatusBarNotificationChipsInteractor") {
+ activeNotificationsInteractor.promotedOngoingNotifications
+ .pairwise(initialValue = emptyList())
+ .collect { (oldNotifs, currentNotifs) ->
+ val removedNotifs = oldNotifs.minus(currentNotifs.toSet())
+ removedNotifs.forEach { removedNotif ->
+ val wasRemoved = promotedNotificationInteractorMap.remove(removedNotif.key)
+ if (wasRemoved == null) {
+ logger.w({
+ "Attempted to remove $str1 from interactor map but it wasn't present"
+ }) {
+ str1 = removedNotif.key
+ }
+ }
+ }
+ currentNotifs.forEach { notif ->
+ val interactor =
+ promotedNotificationInteractorMap.computeIfAbsent(notif.key) {
+ singleNotificationChipInteractorFactory.create(notif)
+ }
+ interactor.setNotification(notif)
+ }
+ logger.d({ "Interactors: $str1" }) {
+ str1 =
+ promotedNotificationInteractorMap.keys.joinToString(separator = " /// ")
+ }
+ promotedNotificationInteractors.value =
+ promotedNotificationInteractorMap.values.toList()
+ }
+ }
+ }
+
+ /**
+ * A flow modeling the notifications that should be shown as chips in the status bar. Emits an
+ * empty list if there are no notifications that should show a status bar chip.
+ */
+ val notificationChips: Flow<List<NotificationChipModel>> =
+ if (StatusBarNotifChips.isEnabled) {
+ // For all our current interactors...
+ promotedNotificationInteractors.flatMapLatest { interactors ->
+ if (interactors.isNotEmpty()) {
+ // Combine each interactor's [notificationChip] flow...
+ val allNotificationChips: List<Flow<NotificationChipModel?>> =
+ interactors.map { interactor -> interactor.notificationChip }
+ combine(allNotificationChips) {
+ // ... and emit just the non-null chips
+ it.filterNotNull()
+ }
+ } else {
+ flowOf(emptyList())
+ }
+ }
+ } else {
+ flowOf(emptyList())
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
new file mode 100644
index 000000000000..5698ee6d1917
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.model
+
+import com.android.systemui.statusbar.StatusBarIconView
+
+/** Modeling all the data needed to render a status bar notification chip. */
+data class NotificationChipModel(val key: String, val statusBarChipIconView: StatusBarIconView)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
index 752674854e2d..9eff627c8714 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
@@ -20,11 +20,10 @@ import android.view.View
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
+import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
-import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -37,7 +36,6 @@ class NotifChipsViewModel
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- activeNotificationsInteractor: ActiveNotificationsInteractor,
private val notifChipsInteractor: StatusBarNotificationChipsInteractor,
) {
/**
@@ -45,19 +43,14 @@ constructor(
* no notifications that should show a status bar chip.
*/
val chips: Flow<List<OngoingActivityChipModel.Shown>> =
- activeNotificationsInteractor.promotedOngoingNotifications.map { notifications ->
- notifications.mapNotNull { it.toChipModel() }
+ notifChipsInteractor.notificationChips.map { notifications ->
+ notifications.map { it.toActivityChipModel() }
}
- /**
- * Converts the notification to the [OngoingActivityChipModel] object. Returns null if the
- * notification has invalid data such that it can't be displayed as a chip.
- */
- private fun ActiveNotificationModel.toChipModel(): OngoingActivityChipModel.Shown? {
+ /** Converts the notification to the [OngoingActivityChipModel] object. */
+ private fun NotificationChipModel.toActivityChipModel(): OngoingActivityChipModel.Shown {
StatusBarNotifChips.assertInNewMode()
- // TODO(b/364653005): Log error if there's no icon view.
- val rawIcon = this.statusBarChipIconView ?: return null
- val icon = OngoingActivityChipModel.ChipIcon.StatusBarView(rawIcon)
+ val icon = OngoingActivityChipModel.ChipIcon.StatusBarView(this.statusBarChipIconView)
// TODO(b/364653005): Use the notification color if applicable.
val colors = ColorsModel.Themed
val onClickListener =
@@ -65,7 +58,9 @@ constructor(
// The notification pipeline needs everything to run on the main thread, so keep
// this event on the main thread.
applicationScope.launch {
- notifChipsInteractor.onPromotedNotificationChipTapped(this@toChipModel.key)
+ notifChipsInteractor.onPromotedNotificationChipTapped(
+ this@toActivityChipModel.key
+ )
}
}
return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt
index 74c7611a6392..eb3bab07b437 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt
@@ -17,6 +17,23 @@
package com.android.systemui.statusbar.chips.notification.domain.interactor
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.chips.statusBarChipsLogger
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
val Kosmos.statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor by
- Kosmos.Fixture { StatusBarNotificationChipsInteractor() }
+ Kosmos.Fixture {
+ StatusBarNotificationChipsInteractor(
+ testScope.backgroundScope,
+ activeNotificationsInteractor,
+ singleNotificationChipInteractorFactory,
+ logBuffer = statusBarChipsLogger,
+ )
+ }
+
+val Kosmos.singleNotificationChipInteractorFactory: SingleNotificationChipInteractor.Factory by
+ Kosmos.Fixture {
+ SingleNotificationChipInteractor.Factory { startingModel ->
+ SingleNotificationChipInteractor(startingModel, logBuffer = statusBarChipsLogger)
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
index 68b28adb4b3a..4bcce8601d64 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
@@ -19,13 +19,8 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
val Kosmos.notifChipsViewModel: NotifChipsViewModel by
Kosmos.Fixture {
- NotifChipsViewModel(
- applicationCoroutineScope,
- activeNotificationsInteractor,
- statusBarNotificationChipsInteractor,
- )
+ NotifChipsViewModel(applicationCoroutineScope, statusBarNotificationChipsInteractor)
}