summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt143
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt207
-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/activity/ActivityManagerModule.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt118
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java2
-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.kt115
-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/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt51
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorFactoryKosmos.kt33
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt7
19 files changed, 1157 insertions, 50 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt
new file mode 100644
index 000000000000..d6ba98d65d15
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.activity.data.repository
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
+import android.app.activityManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.logcatLogBuffer
+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.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ActivityManagerRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val logger = Logger(logcatLogBuffer("ActivityManagerRepositoryTest"), "tag")
+
+ private val Kosmos.underTest by Kosmos.Fixture { realActivityManagerRepository }
+
+ @Test
+ fun createIsAppVisibleFlow_fetchesInitialValue_true() =
+ kosmos.runTest {
+ whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_FOREGROUND)
+
+ val latest by
+ collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG))
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun createIsAppVisibleFlow_fetchesInitialValue_false() =
+ kosmos.runTest {
+ whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
+
+ val latest by
+ collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG))
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun createIsAppVisibleFlow_getsImportanceUpdates() =
+ kosmos.runTest {
+ val latest by
+ collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG))
+
+ val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>()
+ verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any())
+ val listener = listenerCaptor.firstValue
+
+ listener.onUidImportance(THIS_UID, IMPORTANCE_GONE)
+ assertThat(latest).isFalse()
+
+ listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND)
+ assertThat(latest).isTrue()
+
+ listener.onUidImportance(THIS_UID, IMPORTANCE_TOP_SLEEPING)
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun createIsAppVisibleFlow_ignoresUpdatesForOtherUids() =
+ kosmos.runTest {
+ val latest by
+ collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG))
+
+ val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>()
+ verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any())
+ val listener = listenerCaptor.firstValue
+
+ listener.onUidImportance(THIS_UID, IMPORTANCE_GONE)
+ assertThat(latest).isFalse()
+
+ // WHEN another UID becomes foreground
+ listener.onUidImportance(THIS_UID + 2, IMPORTANCE_FOREGROUND)
+
+ // THEN this UID still stays not visible
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun createIsAppVisibleFlow_securityExceptionOnUidRegistration_ok() =
+ kosmos.runTest {
+ whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
+ whenever(activityManager.addOnUidImportanceListener(any(), any()))
+ .thenThrow(SecurityException())
+
+ val latest by
+ collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG))
+
+ // Verify no crash, and we get a value emitted
+ assertThat(latest).isFalse()
+ }
+
+ /** Regression test for b/216248574. */
+ @Test
+ fun createIsAppVisibleFlow_getUidImportanceThrowsException_ok() =
+ kosmos.runTest {
+ whenever(activityManager.getUidImportance(any())).thenThrow(SecurityException())
+
+ val latest by
+ collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG))
+
+ // Verify no crash, and we get a value emitted
+ assertThat(latest).isFalse()
+ }
+
+ companion object {
+ private const val THIS_UID = 558
+ private const val LOG_TAG = "LogTag"
+ }
+}
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..7fed47a4653e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.activity.data.repository.activityManagerRepository
+import com.android.systemui.activity.data.repository.fake
+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.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 factory = kosmos.singleNotificationChipInteractorFactory
+
+ @Test
+ fun notificationChip_startsWithStartingModel() =
+ kosmos.runTest {
+ val icon = mock<StatusBarIconView>()
+ val startingNotif = activeNotificationModel(key = "notif1", statusBarChipIcon = icon)
+
+ val underTest = factory.create(startingNotif)
+
+ 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 =
+ factory.create(
+ activeNotificationModel(key = "notif1", statusBarChipIcon = originalIconView)
+ )
+
+ 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 =
+ factory.create(
+ activeNotificationModel(key = "notif1", statusBarChipIcon = originalIconView)
+ )
+
+ 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 =
+ factory.create(activeNotificationModel(key = "notif1", statusBarChipIcon = null))
+
+ 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 = factory.create(startingNotif)
+ val latest by collectLastValue(underTest.notificationChip)
+ assertThat(latest).isNotNull()
+
+ underTest.setNotification(
+ activeNotificationModel(key = "notif1", statusBarChipIcon = null)
+ )
+
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun notificationChip_appIsVisibleOnCreation_emitsNull() =
+ kosmos.runTest {
+ activityManagerRepository.fake.startingIsAppVisibleValue = true
+
+ val underTest =
+ factory.create(
+ activeNotificationModel(key = "notif", uid = UID, statusBarChipIcon = mock())
+ )
+
+ val latest by collectLastValue(underTest.notificationChip)
+
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun notificationChip_appNotVisibleOnCreation_emitsValue() =
+ kosmos.runTest {
+ activityManagerRepository.fake.startingIsAppVisibleValue = false
+
+ val underTest =
+ factory.create(
+ activeNotificationModel(key = "notif", uid = UID, statusBarChipIcon = mock())
+ )
+
+ val latest by collectLastValue(underTest.notificationChip)
+
+ assertThat(latest).isNotNull()
+ }
+
+ @Test
+ fun notificationChip_hidesWhenAppIsVisible() =
+ kosmos.runTest {
+ val underTest =
+ factory.create(
+ activeNotificationModel(key = "notif", uid = UID, statusBarChipIcon = mock())
+ )
+
+ val latest by collectLastValue(underTest.notificationChip)
+
+ activityManagerRepository.fake.setIsAppVisible(UID, false)
+ assertThat(latest).isNotNull()
+
+ activityManagerRepository.fake.setIsAppVisible(UID, true)
+ assertThat(latest).isNull()
+
+ activityManagerRepository.fake.setIsAppVisible(UID, false)
+ assertThat(latest).isNotNull()
+ }
+
+ // Note: This test is theoretically impossible because the notification key should contain the
+ // UID, so if the UID changes then the key would also change and a new interactor would be
+ // created. But, test it just in case.
+ @Test
+ fun notificationChip_updatedUid_rechecksAppVisibility_oldObserverUnregistered() =
+ kosmos.runTest {
+ activityManagerRepository.fake.startingIsAppVisibleValue = false
+
+ val hiddenUid = 100
+ val shownUid = 101
+
+ val underTest =
+ factory.create(
+ activeNotificationModel(
+ key = "notif",
+ uid = hiddenUid,
+ statusBarChipIcon = mock(),
+ )
+ )
+ val latest by collectLastValue(underTest.notificationChip)
+ assertThat(latest).isNotNull()
+
+ // WHEN the notif gets a new UID that starts as visible
+ activityManagerRepository.fake.startingIsAppVisibleValue = true
+ underTest.setNotification(
+ activeNotificationModel(key = "notif", uid = shownUid, statusBarChipIcon = mock())
+ )
+
+ // THEN we re-fetch the app visibility state with the new UID, and since that UID is
+ // visible, we hide the chip
+ assertThat(latest).isNull()
+ }
+
+ companion object {
+ private const val UID = 885
+ }
+}
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 e5d2cf65a389..1d7f25784327 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
@@ -83,7 +83,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/activity/ActivityManagerModule.kt b/packages/SystemUI/src/com/android/systemui/activity/ActivityManagerModule.kt
new file mode 100644
index 000000000000..db315e4e0bf7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/activity/ActivityManagerModule.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.activity
+
+import com.android.systemui.activity.data.repository.ActivityManagerRepository
+import com.android.systemui.activity.data.repository.ActivityManagerRepositoryImpl
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface ActivityManagerModule {
+ @Binds
+ @SysUISingleton
+ fun activityManagerRepository(impl: ActivityManagerRepositoryImpl): ActivityManagerRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt
new file mode 100644
index 000000000000..94614b70beda
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.activity.data.repository
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.core.Logger
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.onStart
+
+/** Repository for interfacing with [ActivityManager]. */
+interface ActivityManagerRepository {
+ /**
+ * Given a UID, creates a flow that emits true when the process with the given UID is visible to
+ * the user and false otherwise.
+ *
+ * @param identifyingLogTag a tag identifying who created this flow, used for logging.
+ */
+ fun createIsAppVisibleFlow(
+ creationUid: Int,
+ logger: Logger,
+ identifyingLogTag: String,
+ ): Flow<Boolean>
+}
+
+@SysUISingleton
+class ActivityManagerRepositoryImpl
+@Inject
+constructor(
+ @Background private val backgroundContext: CoroutineContext,
+ private val activityManager: ActivityManager,
+) : ActivityManagerRepository {
+ override fun createIsAppVisibleFlow(
+ creationUid: Int,
+ logger: Logger,
+ identifyingLogTag: String,
+ ): Flow<Boolean> {
+ return conflatedCallbackFlow {
+ val listener =
+ object : ActivityManager.OnUidImportanceListener {
+ override fun onUidImportance(uid: Int, importance: Int) {
+ if (uid != creationUid) {
+ return
+ }
+ val isAppVisible = isAppVisibleToUser(importance)
+ logger.d({
+ "$str1: #onUidImportance. importance=$int1, isAppVisible=$bool1"
+ }) {
+ str1 = identifyingLogTag
+ int1 = importance
+ bool1 = isAppVisible
+ }
+ trySend(isAppVisible)
+ }
+ }
+ try {
+ // TODO(b/286258140): Replace this with the #addOnUidImportanceListener
+ // overload that filters to certain UIDs.
+ activityManager.addOnUidImportanceListener(listener, IMPORTANCE_CUTPOINT)
+ } catch (e: SecurityException) {
+ logger.e({ "$str1: Security exception on #addOnUidImportanceListener" }, e) {
+ str1 = identifyingLogTag
+ }
+ }
+
+ awaitClose { activityManager.removeOnUidImportanceListener(listener) }
+ }
+ .distinctUntilChanged()
+ .onStart {
+ try {
+ val isVisibleOnStart =
+ isAppVisibleToUser(activityManager.getUidImportance(creationUid))
+ logger.d({ "$str1: Starting UID observation. isAppVisible=$bool1" }) {
+ str1 = identifyingLogTag
+ bool1 = isVisibleOnStart
+ }
+ emit(isVisibleOnStart)
+ } catch (e: SecurityException) {
+ logger.e({ "$str1: Security exception on #getUidImportance" }, e) {
+ str1 = identifyingLogTag
+ }
+ emit(false)
+ }
+ }
+ .flowOn(backgroundContext)
+ }
+
+ /** Returns true if the given [importance] represents an app that's visible to the user. */
+ private fun isAppVisibleToUser(importance: Int): Boolean {
+ return importance <= IMPORTANCE_CUTPOINT
+ }
+
+ companion object {
+ private const val IMPORTANCE_CUTPOINT = IMPORTANCE_FOREGROUND
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 6a777ee7417b..d6f8957ace33 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -32,6 +32,7 @@ import com.android.systemui.BootCompleteCacheImpl;
import com.android.systemui.CameraProtectionModule;
import com.android.systemui.CoreStartable;
import com.android.systemui.SystemUISecondaryUserService;
+import com.android.systemui.activity.ActivityManagerModule;
import com.android.systemui.ambient.dagger.AmbientModule;
import com.android.systemui.appops.dagger.AppOpsModule;
import com.android.systemui.assist.AssistModule;
@@ -198,6 +199,7 @@ import javax.inject.Named;
* may not appreciate that.
*/
@Module(includes = {
+ ActivityManagerModule.class,
AmbientModule.class,
AppOpsModule.class,
AssistModule.class,
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..087b51032fcf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.activity.data.repository.ActivityManagerRepository
+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.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+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.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class SingleNotificationChipInteractor
+@AssistedInject
+constructor(
+ @Assisted startingModel: ActiveNotificationModel,
+ private val activityManagerRepository: ActivityManagerRepository,
+ @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
+ }
+
+ private val uid: Flow<Int> = _notificationModel.map { it.uid }
+
+ /** True if the application managing the notification is visible to the user. */
+ private val isAppVisible: Flow<Boolean> =
+ uid.flatMapLatest { currentUid ->
+ activityManagerRepository.createIsAppVisibleFlow(currentUid, logger, extraLogTag)
+ }
+
+ /**
+ * Emits this notification's status bar chip, or null if this notification shouldn't show a
+ * status bar chip.
+ */
+ val notificationChip: Flow<NotificationChipModel?> =
+ combine(_notificationModel, isAppVisible) { notif, isAppVisible ->
+ if (isAppVisible) {
+ // If the app that posted this notification is visible, we want to hide the chip
+ // because information between the status bar chip and the app itself could be
+ // out-of-sync (like a timer that's slightly off)
+ null
+ } else {
+ 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/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index aac2cd1755d0..78926c78a368 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -25,6 +25,7 @@ import android.app.UidObserver
import android.content.Context
import android.view.View
import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.CoreStartable
import com.android.systemui.Dumpable
@@ -58,7 +59,6 @@ import java.io.PrintWriter
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
/** A controller to handle the ongoing call chip in the collapsed status bar. */
@SysUISingleton
@@ -122,9 +122,9 @@ constructor(
entry.sbn.uid,
entry.sbn.notification.extras.getInt(
Notification.EXTRA_CALL_TYPE,
- -1
+ -1,
) == CALL_TYPE_ONGOING,
- statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false
+ statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false,
)
if (newOngoingCallInfo == callNotificationInfo) {
return
@@ -236,7 +236,7 @@ constructor(
bool1 = Flags.statusBarCallChipNotificationIcon()
bool2 = currentInfo.notificationIconView != null
},
- { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" }
+ { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" },
)
val icon =
if (Flags.statusBarCallChipNotificationIcon()) {
@@ -288,7 +288,7 @@ constructor(
str1 = notifModel.callType.name
bool1 = notifModel.statusBarChipIconView != null
},
- { "NotifInteractorCallModel: key=$str1 when=$long1 callType=$str2 hasIcon=$bool1" }
+ { "NotifInteractorCallModel: key=$str1 when=$long1 callType=$str2 hasIcon=$bool1" },
)
val newOngoingCallInfo =
@@ -299,7 +299,7 @@ constructor(
notifModel.contentIntent,
notifModel.uid,
isOngoing = true,
- statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false
+ statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false,
)
if (newOngoingCallInfo == callNotificationInfo) {
return
@@ -378,7 +378,7 @@ constructor(
ActivityTransitionAnimator.Controller.fromView(
backgroundView,
InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
- )
+ ),
)
}
}
@@ -455,7 +455,7 @@ constructor(
/** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
val isOngoing: Boolean,
/** True if the user has swiped away the status bar while in this phone call. */
- val statusBarSwipedAway: Boolean
+ val statusBarSwipedAway: Boolean,
) {
/**
* Returns true if the notification information has a valid call start time. See
@@ -472,6 +472,9 @@ constructor(
/**
* Observer to tell us when the app that posted the ongoing call notification is visible so that
* we don't show the call chip at the same time (since the timers could be out-of-sync).
+ *
+ * For a more recommended architecture implementation, see
+ * [com.android.systemui.activity.data.repository.ActivityManagerRepository].
*/
inner class CallAppUidObserver : UidObserver() {
/** True if the application managing the call is visible to the user. */
@@ -512,7 +515,7 @@ constructor(
uidObserver,
ActivityManager.UID_OBSERVER_PROCSTATE,
ActivityManager.PROCESS_STATE_UNKNOWN,
- context.opPackageName
+ context.opPackageName,
)
isRegistered = true
} catch (se: SecurityException) {
@@ -537,7 +540,7 @@ constructor(
uid: Int,
procState: Int,
procStateSeq: Long,
- capability: Int
+ capability: Int,
) {
val currentCallAppUid = callAppUid ?: return
if (uid != currentCallAppUid) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt
new file mode 100644
index 000000000000..a6e71333c816
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.activity.data.repository
+
+import android.app.activityManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.log.core.Logger
+import kotlinx.coroutines.flow.MutableStateFlow
+
+val Kosmos.activityManagerRepository by Kosmos.Fixture { FakeActivityManagerRepository() }
+
+val Kosmos.realActivityManagerRepository by
+ Kosmos.Fixture { ActivityManagerRepositoryImpl(testDispatcher, activityManager) }
+
+class FakeActivityManagerRepository : ActivityManagerRepository {
+ private val uidFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>()
+
+ var startingIsAppVisibleValue = false
+
+ override fun createIsAppVisibleFlow(
+ creationUid: Int,
+ logger: Logger,
+ identifyingLogTag: String,
+ ): MutableStateFlow<Boolean> {
+ val newFlow = MutableStateFlow(startingIsAppVisibleValue)
+ uidFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow)
+ return newFlow
+ }
+
+ fun setIsAppVisible(uid: Int, isAppVisible: Boolean) {
+ uidFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible }
+ }
+}
+
+val ActivityManagerRepository.fake
+ get() = this as FakeActivityManagerRepository
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorFactoryKosmos.kt
new file mode 100644
index 000000000000..1c095e11dffa
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorFactoryKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.activity.data.repository.activityManagerRepository
+import com.android.systemui.activity.data.repository.fake
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.chips.statusBarChipsLogger
+
+val Kosmos.singleNotificationChipInteractorFactory: SingleNotificationChipInteractor.Factory by
+ Kosmos.Fixture {
+ SingleNotificationChipInteractor.Factory { startingModel ->
+ SingleNotificationChipInteractor(
+ startingModel,
+ activityManagerRepository.fake,
+ logBuffer = statusBarChipsLogger,
+ )
+ }
+ }
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..03e9f3d52ca3 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,16 @@
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,
+ )
+ }
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)
}