Create HeadsUpInteractor backed by HeadsUpManager
Fixes: 328390331
Test: atest NotificationListViewModelTest
HeadsUpNotificationInteractorTest SceneContainerStartableTest
Flag: ACONFIG com.android.systemui.notifications_heads_up_refactor DEVELOPMENT
Change-Id: I76d466cff68c71a489a0d5b4ad82eb90563b464c
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index cc66f8b..f018cc1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -51,6 +51,8 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -175,10 +177,12 @@
transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone)
assertThat(isVisible).isFalse()
- kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = true
+ kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+ buildNotificationRows(isPinned = true)
assertThat(isVisible).isTrue()
- kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = false
+ kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+ buildNotificationRows(isPinned = false)
assertThat(isVisible).isFalse()
}
@@ -1070,4 +1074,17 @@
return transitionStateFlow
}
+
+ private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> =
+ setOf(
+ fakeHeadsUpRowRepository(key = "0", isPinned = isPinned),
+ fakeHeadsUpRowRepository(key = "1", isPinned = isPinned),
+ fakeHeadsUpRowRepository(key = "2", isPinned = isPinned),
+ fakeHeadsUpRowRepository(key = "3", isPinned = isPinned),
+ )
+
+ private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean) =
+ FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
+ this.isPinned.value = isPinned
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
new file mode 100644
index 0000000..bba9991
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.domain.interactor
+
+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.kosmos.testScope
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+class HeadsUpNotificationInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val repository = kosmos.headsUpNotificationRepository
+
+ private val underTest = kosmos.headsUpNotificationInteractor
+
+ @Test
+ fun hasPinnedRows_emptyList_false() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+
+ assertThat(hasPinnedRows).isFalse()
+ }
+
+ @Test
+ fun hasPinnedRows_noPinnedRows_false() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+ // WHEN no pinned rows are set
+ repository.setNotifications(
+ fakeHeadsUpRowRepository("key 0"),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ runCurrent()
+
+ // THEN hasPinnedRows is false
+ assertThat(hasPinnedRows).isFalse()
+ }
+
+ @Test
+ fun hasPinnedRows_hasPinnedRows_true() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+ // WHEN a pinned rows is set
+ repository.setNotifications(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ runCurrent()
+
+ // THEN hasPinnedRows is true
+ assertThat(hasPinnedRows).isTrue()
+ }
+
+ @Test
+ fun hasPinnedRows_rowGetsPinned_true() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+ // GIVEN no rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0"),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // WHEN a row gets pinned
+ rows[0].isPinned.value = true
+ runCurrent()
+
+ // THEN hasPinnedRows updates to true
+ assertThat(hasPinnedRows).isTrue()
+ }
+
+ @Test
+ fun hasPinnedRows_rowGetsUnPinned_false() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+ // GIVEN one row is pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // THEN that row gets unpinned
+ rows[0].isPinned.value = false
+ runCurrent()
+
+ // THEN hasPinnedRows updates to false
+ assertThat(hasPinnedRows).isFalse()
+ }
+
+ @Test
+ fun pinnedRows_noRows_isEmpty() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+
+ assertThat(pinnedHeadsUpRows).isEmpty()
+ }
+
+ @Test
+ fun pinnedRows_noPinnedRows_isEmpty() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // WHEN no rows are pinned
+ repository.setNotifications(
+ fakeHeadsUpRowRepository("key 0"),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ runCurrent()
+
+ // THEN all rows are filtered
+ assertThat(pinnedHeadsUpRows).isEmpty()
+ }
+
+ @Test
+ fun pinnedRows_hasPinnedRows_containsPinnedRows() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // WHEN some rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1", isPinned = true),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // THEN the unpinned rows are filtered
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1])
+ }
+
+ @Test
+ fun pinnedRows_rowGetsPinned_containsPinnedRows() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // GIVEN some rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1", isPinned = true),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // WHEN all rows gets pinned
+ rows[2].isPinned.value = true
+ runCurrent()
+
+ // THEN no rows are filtered
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+ }
+
+ @Test
+ fun pinnedRows_allRowsPinned_containsAllRows() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // WHEN all rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1", isPinned = true),
+ fakeHeadsUpRowRepository("key 2", isPinned = true),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // THEN no rows are filtered
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+ }
+
+ @Test
+ fun pinnedRows_rowGetsUnPinned_containsPinnedRows() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // GIVEN all rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1", isPinned = true),
+ fakeHeadsUpRowRepository("key 2", isPinned = true),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // WHEN a row gets unpinned
+ rows[0].isPinned.value = false
+ runCurrent()
+
+ // THEN the unpinned row is filtered
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[1], rows[2])
+ }
+
+ @Test
+ fun pinnedRows_rowGetsPinnedAndUnPinned_containsTheSameInstance() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0"),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ rows[0].isPinned.value = true
+ runCurrent()
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
+
+ rows[0].isPinned.value = false
+ runCurrent()
+ assertThat(pinnedHeadsUpRows).isEmpty()
+
+ rows[0].isPinned.value = true
+ runCurrent()
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
+ }
+
+ private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) =
+ FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
+ this.isPinned.value = isPinned
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 9cb920a..fe4abde 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -193,6 +193,7 @@
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.notification.stack.AnimationProperties;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -4381,6 +4382,10 @@
@Override
public void onHeadsUpPinned(NotificationEntry entry) {
+ if (NotificationsHeadsUpRefactor.isEnabled()) {
+ return;
+ }
+
if (!isKeyguardShowing()) {
mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, true);
}
@@ -4388,6 +4393,9 @@
@Override
public void onHeadsUpUnPinned(NotificationEntry entry) {
+ if (NotificationsHeadsUpRefactor.isEnabled()) {
+ return;
+ }
// When we're unpinning the notification via active edge they remain heads-upped,
// we need to make sure that an animation happens in this case, otherwise the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
index e5e5292..2b0d2aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
@@ -15,8 +15,8 @@
*/
package com.android.systemui.statusbar.notification.data
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepositoryImpl
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
import dagger.Binds
import dagger.Module
@@ -27,8 +27,5 @@
]
)
interface NotificationDataLayerModule {
- @Binds
- fun bindHeadsUpNotificationRepository(
- impl: HeadsUpNotificationRepositoryImpl
- ): HeadsUpNotificationRepository
+ @Binds fun bindHeadsUpNotificationRepository(impl: HeadsUpManagerPhone): HeadsUpRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt
deleted file mode 100644
index d60ee98..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.notification.data.repository
-
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.policy.HeadsUpManager
-import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
-import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-
-class HeadsUpNotificationRepositoryImpl
-@Inject
-constructor(
- headsUpManager: HeadsUpManager,
-) : HeadsUpNotificationRepository {
- override val hasPinnedHeadsUp: Flow<Boolean> = conflatedCallbackFlow {
- val listener =
- object : OnHeadsUpChangedListener {
- override fun onHeadsUpPinnedModeChanged(inPinnedMode: Boolean) {
- trySend(headsUpManager.hasPinnedHeadsUp())
- }
-
- override fun onHeadsUpPinned(entry: NotificationEntry?) {
- trySend(headsUpManager.hasPinnedHeadsUp())
- }
-
- override fun onHeadsUpUnPinned(entry: NotificationEntry?) {
- trySend(headsUpManager.hasPinnedHeadsUp())
- }
-
- override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
- trySend(headsUpManager.hasPinnedHeadsUp())
- }
- }
- trySend(headsUpManager.hasPinnedHeadsUp())
- headsUpManager.addListener(listener)
- awaitClose { headsUpManager.removeListener(listener) }
- }
-}
-
-interface HeadsUpNotificationRepository {
- val hasPinnedHeadsUp: Flow<Boolean>
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt
new file mode 100644
index 0000000..ed8c056
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.notification.data.repository
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A repository of currently displayed heads up notifications.
+ *
+ * This repository serves as a boundary between the
+ * [com.android.systemui.statusbar.policy.HeadsUpManager] and the modern notifications presentation
+ * codebase.
+ */
+interface HeadsUpRepository {
+
+ /**
+ * True if we are exiting the headsUp pinned mode, and some notifications might still be
+ * animating out. This is used to keep the touchable regions in a reasonable state.
+ */
+ val headsUpAnimatingAway: Flow<Boolean>
+
+ /** The heads up row that should be displayed on top. */
+ val topHeadsUpRow: Flow<HeadsUpRowRepository?>
+
+ /** Set of currently active top-level heads up rows to be displayed. */
+ val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt
new file mode 100644
index 0000000..7b40812
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.notification.data.repository
+
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import kotlinx.coroutines.flow.StateFlow
+
+/** Representation of a top-level heads up row. */
+interface HeadsUpRowRepository : HeadsUpRowKey {
+ /**
+ * The key for this notification. Guaranteed to be immutable and unique.
+ *
+ * @see com.android.systemui.statusbar.notification.collection.NotificationEntry.getKey
+ */
+ val key: String
+
+ /** A key to identify this row in the view hierarchy. */
+ val elementKey: Any
+
+ /** Whether this notification is "pinned", meaning that it should stay on top of the screen. */
+ val isPinned: StateFlow<Boolean>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index 5c8f354..d1dd7b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -14,14 +14,59 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.statusbar.notification.domain.interactor
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
-class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpNotificationRepository) {
+class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpRepository) {
+
+ val topHeadsUpRow: Flow<HeadsUpRowKey?> = repository.topHeadsUpRow
+
+ /** Set of currently pinned top-level heads up rows to be displayed. */
+ val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> =
+ repository.activeHeadsUpRows.flatMapLatest { repositories ->
+ if (repositories.isNotEmpty()) {
+ val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> =
+ repositories.map { repo -> repo.isPinned.map { isPinned -> repo to isPinned } }
+ combine(toCombine) { pairs ->
+ pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet()
+ }
+ } else {
+ // if the set is empty, there are no flows to combine
+ flowOf(emptySet())
+ }
+ }
+
+ /** Are there any pinned heads up rows to display? */
+ val hasPinnedRows: Flow<Boolean> =
+ repository.activeHeadsUpRows.flatMapLatest { rows ->
+ if (rows.isNotEmpty()) {
+ combine(rows.map { it.isPinned }) { pins -> pins.any { it } }
+ } else {
+ // if the set is empty, there are no flows to combine
+ flowOf(false)
+ }
+ }
+
val isHeadsUpOrAnimatingAway: Flow<Boolean> =
- // TODO(b/296118689): Needs to include the animating away state.
- repository.hasPinnedHeadsUp
+ combine(hasPinnedRows, repository.headsUpAnimatingAway) { hasPinnedRows, animatingAway ->
+ hasPinnedRows || animatingAway
+ }
+
+ fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor =
+ HeadsUpRowInteractor(key as HeadsUpRowRepository)
+ fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey
}
+
+class HeadsUpRowInteractor(repository: HeadsUpRowRepository)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.kt
new file mode 100644
index 0000000..8dc395d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.notification.shared
+
+/**
+ * A unique key representing a top-level heads up notification.
+ *
+ * @see com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+ */
+interface HeadsUpRowKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index f2c593d..fb52838 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -5405,7 +5405,7 @@
/**
* @param topHeadsUpRow the first headsUp row in z-order.
*/
- public void setTopHeadsUpRow(ExpandableNotificationRow topHeadsUpRow) {
+ public void setTopHeadsUpRow(@Nullable ExpandableNotificationRow topHeadsUpRow) {
mTopHeadsUpRow = topHeadsUpRow;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 8ed1ca2..7bdd3f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -126,6 +126,7 @@
import com.android.systemui.statusbar.notification.row.NotificationGuts;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.row.NotificationSnooze;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor;
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -685,11 +686,13 @@
new OnHeadsUpChangedListener() {
@Override
public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
mView.setInHeadsUpPinnedMode(inPinnedMode);
}
@Override
public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
NotificationEntry topEntry = mHeadsUpManager.getTopEntry();
mView.setTopHeadsUpRow(topEntry != null ? topEntry.getRow() : null);
generateHeadsUpAnimation(entry, isHeadsUp);
@@ -870,7 +873,9 @@
});
}
- mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
+ if (!NotificationsHeadsUpRefactor.isEnabled()) {
+ mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
+ }
mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed);
mDynamicPrivacyController.addListener(mDynamicPrivacyControllerListener);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 6b30393..97cbbe8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -36,6 +36,7 @@
import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
@@ -44,6 +45,7 @@
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import com.android.systemui.statusbar.notification.ui.viewbinder.HeadsUpNotificationViewBinder
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.util.kotlin.awaitCancellationThenDispose
import com.android.systemui.util.kotlin.getOrNull
@@ -71,6 +73,7 @@
private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
private val configuration: ConfigurationState,
private val falsingManager: FalsingManager,
+ private val hunBinder: HeadsUpNotificationViewBinder,
private val iconAreaController: NotificationIconAreaController,
private val loggerOptional: Optional<NotificationStatsLogger>,
private val metricsLogger: MetricsLogger,
@@ -92,6 +95,9 @@
view.repeatWhenAttached {
lifecycleScope.launch {
+ if (NotificationsHeadsUpRefactor.isEnabled) {
+ launch { hunBinder.bindHeadsUpNotifications(view) }
+ }
launch { bindShelf(shelf) }
bindHideList(viewController, viewModel, hiderTracker)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt
new file mode 100644
index 0000000..ec5e5be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.notification.stack.ui.viewmodel
+
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpRowInteractor
+
+class HeadsUpRowViewModel(headsUpRowInteractor: HeadsUpRowInteractor)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 4744fcb..a6ca027 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -17,12 +17,16 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
@@ -53,6 +57,8 @@
val logger: Optional<NotificationLoggerViewModel>,
activeNotificationsInteractor: ActiveNotificationsInteractor,
notificationStackInteractor: NotificationStackInteractor,
+ private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
+ keyguardInteractor: KeyguardInteractor,
remoteInputInteractor: RemoteInputInteractor,
seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
@@ -212,4 +218,41 @@
activeNotificationsInteractor.hasNonClearableSilentNotifications
}
}
+
+ val topHeadsUpRow: Flow<HeadsUpRowKey?> by lazy {
+ if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(null)
+ } else {
+ headsUpNotificationInteractor.topHeadsUpRow
+ }
+ }
+
+ val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
+ if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(emptySet())
+ } else {
+ headsUpNotificationInteractor.pinnedHeadsUpRows
+ }
+ }
+
+ val headsUpAnimationsEnabled: Flow<Boolean> by lazy {
+ combine(keyguardInteractor.isKeyguardShowing, shadeInteractor.isShadeFullyExpanded) {
+ (isKeyguardShowing, isShadeFullyExpanded) ->
+ // TODO(b/325936094) use isShadeFullyCollapsed instead
+ !isKeyguardShowing && !isShadeFullyExpanded
+ }
+ }
+
+ val hasPinnedHeadsUpRow: Flow<Boolean> by lazy {
+ if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ headsUpNotificationInteractor.hasPinnedRows
+ }
+ }
+
+ // TODO(b/325936094) use it for the text displayed in the StatusBar
+ fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowViewModel =
+ HeadsUpRowViewModel(headsUpNotificationInteractor.headsUpRow(key))
+ fun elementKeyFor(key: HeadsUpRowKey): Any = headsUpNotificationInteractor.elementKeyFor(key)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
new file mode 100644
index 0000000..cb360fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.notification.ui.viewbinder
+
+import android.util.Log
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+private const val TAG = "HunBinder"
+private val DEBUG = true // Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)
+
+class HeadsUpNotificationViewBinder
+@Inject
+constructor(private val viewModel: NotificationListViewModel) {
+ suspend fun bindHeadsUpNotifications(parentView: NotificationStackScrollLayout): Unit =
+ coroutineScope {
+ launch {
+ var previousKeys = emptySet<HeadsUpRowKey>()
+ viewModel.pinnedHeadsUpRows
+ .sample(viewModel.headsUpAnimationsEnabled, ::Pair)
+ .collect { (newKeys, animationsEnabled) ->
+ if (DEBUG) {
+ Log.d(TAG, "update:$newKeys")
+ }
+
+ val added = newKeys - previousKeys
+ val removed = previousKeys - newKeys
+ previousKeys = newKeys
+
+ if (animationsEnabled) {
+ added.forEach { key ->
+ parentView.generateHeadsUpAnimation(
+ obtainView(key),
+ /* isHeadsUp = */ true
+ )
+ }
+ removed.forEach { key ->
+ val row = obtainView(key)
+ parentView.generateHeadsUpAnimation(row, /* isHeadsUp = */ false)
+ row.setHeadsUpIsVisible()
+ }
+ }
+ }
+ }
+ launch {
+ viewModel.topHeadsUpRow.collect { key ->
+ parentView.setTopHeadsUpRow(key?.let(::obtainView))
+ }
+ }
+ launch {
+ viewModel.hasPinnedHeadsUpRow.collect { parentView.setInHeadsUpPinnedMode(it) }
+ }
+ }
+
+ private fun obtainView(key: HeadsUpRowKey): ExpandableNotificationRow {
+ return viewModel.elementKeyFor(key) as ExpandableNotificationRow
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 86bb844..3f200d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -22,6 +22,7 @@
import android.content.res.Resources;
import android.graphics.Region;
import android.os.Handler;
+import android.util.ArrayMap;
import android.util.Pools;
import androidx.collection.ArraySet;
@@ -40,6 +41,8 @@
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
@@ -59,13 +62,21 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
import java.util.Stack;
import javax.inject.Inject;
+import kotlinx.coroutines.flow.Flow;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
/** A implementation of HeadsUpManager for phone. */
@SysUISingleton
-public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUpChangedListener {
+public class HeadsUpManagerPhone extends BaseHeadsUpManager implements
+ HeadsUpRepository, OnHeadsUpChangedListener {
private static final String TAG = "HeadsUpManagerPhone";
@VisibleForTesting
@@ -74,15 +85,20 @@
private final GroupMembershipManager mGroupMembershipManager;
private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>();
private final VisualStabilityProvider mVisualStabilityProvider;
- private boolean mReleaseOnExpandFinish;
+ // TODO(b/328393698) move the topHeadsUpRow logic to an interactor
+ private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow =
+ StateFlowKt.MutableStateFlow(null);
+ private final MutableStateFlow<Set<HeadsUpRowRepository>> mHeadsUpNotificationRows =
+ StateFlowKt.MutableStateFlow(new HashSet<>());
+ private final MutableStateFlow<Boolean> mHeadsUpGoingAway = StateFlowKt.MutableStateFlow(false);
+ private boolean mReleaseOnExpandFinish;
private boolean mTrackingHeadsUp;
private final HashSet<String> mSwipedOutKeys = new HashSet<>();
private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
= new ArraySet<>();
private boolean mIsExpanded;
- private boolean mHeadsUpGoingAway;
private int mStatusBarState;
private AnimationStateHandler mAnimationStateHandler;
private int mHeadsUpInset;
@@ -248,7 +264,7 @@
if (isExpanded != mIsExpanded) {
mIsExpanded = isExpanded;
if (isExpanded) {
- mHeadsUpGoingAway = false;
+ mHeadsUpGoingAway.setValue(false);
}
}
}
@@ -259,17 +275,17 @@
*/
@Override
public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
- if (headsUpGoingAway != mHeadsUpGoingAway) {
- mHeadsUpGoingAway = headsUpGoingAway;
+ if (headsUpGoingAway != mHeadsUpGoingAway.getValue()) {
for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) {
listener.onHeadsUpGoingAwayStateChanged(headsUpGoingAway);
}
+ mHeadsUpGoingAway.setValue(headsUpGoingAway);
}
}
@Override
public boolean isHeadsUpGoingAway() {
- return mHeadsUpGoingAway;
+ return mHeadsUpGoingAway.getValue();
}
/**
@@ -288,6 +304,7 @@
} else {
headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)");
}
+ onEntryUpdated(headsUpEntry);
}
}
@@ -387,11 +404,35 @@
}
@Override
+ protected void onEntryAdded(HeadsUpEntry headsUpEntry) {
+ super.onEntryAdded(headsUpEntry);
+ updateTopHeadsUpFlow();
+ updateHeadsUpFlow();
+ }
+
+ @Override
+ protected void onEntryUpdated(HeadsUpEntry headsUpEntry) {
+ super.onEntryUpdated(headsUpEntry);
+ // no need to update the list here
+ updateTopHeadsUpFlow();
+ }
+
+ @Override
protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
super.onEntryRemoved(headsUpEntry);
if (!NotificationsHeadsUpRefactor.isEnabled()) {
mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
}
+ updateTopHeadsUpFlow();
+ updateHeadsUpFlow();
+ }
+
+ private void updateTopHeadsUpFlow() {
+ mTopHeadsUpRow.setValue((HeadsUpRowRepository) getTopHeadsUpEntry());
+ }
+
+ private void updateHeadsUpFlow() {
+ mHeadsUpNotificationRows.setValue(new HashSet<>(getHeadsUpEntryPhoneMap().values()));
}
@Override
@@ -415,6 +456,12 @@
///////////////////////////////////////////////////////////////////////////////////////////////
// Private utility methods:
+ @NonNull
+ private ArrayMap<String, HeadsUpEntryPhone> getHeadsUpEntryPhoneMap() {
+ //noinspection unchecked
+ return (ArrayMap<String, HeadsUpEntryPhone>) ((ArrayMap) mHeadsUpEntryMap);
+ }
+
@Nullable
private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
return (HeadsUpEntryPhone) mHeadsUpEntryMap.get(key);
@@ -422,7 +469,11 @@
@Nullable
private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
- return (HeadsUpEntryPhone) getTopHeadsUpEntry();
+ if (NotificationsHeadsUpRefactor.isEnabled()) {
+ return (HeadsUpEntryPhone) mTopHeadsUpRow.getValue();
+ } else {
+ return (HeadsUpEntryPhone) getTopHeadsUpEntry();
+ }
}
@Override
@@ -439,12 +490,32 @@
return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key);
}
+ @Override
+ @NonNull
+ public Flow<HeadsUpRowRepository> getTopHeadsUpRow() {
+ return mTopHeadsUpRow;
+ }
+
+ @Override
+ @NonNull
+ public Flow<Set<HeadsUpRowRepository>> getActiveHeadsUpRows() {
+ return mHeadsUpNotificationRows;
+ }
+
+ @Override
+ @NonNull
+ public Flow<Boolean> getHeadsUpAnimatingAway() {
+ return mHeadsUpGoingAway;
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////////
// HeadsUpEntryPhone:
- protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry {
+ protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry implements
+ HeadsUpRowRepository {
private boolean mGutsShownPinned;
+ private final MutableStateFlow<Boolean> mIsPinned = StateFlowKt.MutableStateFlow(false);
/**
* If the time this entry has been on was extended
@@ -465,6 +536,25 @@
}
@Override
+ @NonNull
+ public String getKey() {
+ return requireEntry().getKey();
+ }
+
+ @Override
+ @NonNull
+ public StateFlow<Boolean> isPinned() {
+ return mIsPinned;
+ }
+
+ @Override
+ protected void setRowPinned(boolean pinned) {
+ // TODO(b/327624082): replace this super call with a ViewBinder
+ super.setRowPinned(pinned);
+ mIsPinned.setValue(pinned);
+ }
+
+ @Override
protected Runnable createRemoveRunnable(NotificationEntry entry) {
return () -> {
if (!mVisualStabilityProvider.isReorderingAllowed()
@@ -539,6 +629,17 @@
protected long calculateFinishTime() {
return super.calculateFinishTime() + (extended ? mExtensionTime : 0);
}
+
+ @Override
+ @NonNull
+ public Object getElementKey() {
+ return requireEntry().getRow();
+ }
+
+ private NotificationEntry requireEntry() {
+ /* check if */ NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode();
+ return Objects.requireNonNull(mEntry);
+ }
}
private final StateListener mStatusBarStateListener = new StateListener() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 6f7e046..20a82a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -172,6 +172,7 @@
// Add new entry and begin managing it
mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry);
onEntryAdded(headsUpEntry);
+ // TODO(b/328390331) move accessibility events to the view layer
entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
entry.setIsHeadsUpEntry(true);
@@ -232,7 +233,7 @@
// with the groupmanager
return;
}
-
+ // TODO(b/328390331) move accessibility events to the view layer
headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
if (shouldHeadsUpAgain) {
@@ -332,15 +333,15 @@
if (!isPinned) {
headsUpEntry.mWasUnpinned = true;
}
- if (headsUpEntry.isPinned() != isPinned) {
- headsUpEntry.setPinned(isPinned);
+ if (headsUpEntry.isRowPinned() != isPinned) {
+ headsUpEntry.setRowPinned(isPinned);
updatePinnedMode();
if (isPinned && entry.getSbn() != null) {
mUiEventLogger.logWithInstanceId(
NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(),
entry.getSbn().getPackageName(), entry.getSbn().getInstanceId());
}
- // TODO(b/325936094) convert these listeners to collecting a flow
+ // TODO(b/325936094) use the isPinned Flow instead
for (OnHeadsUpChangedListener listener : mListeners) {
if (isPinned) {
listener.onHeadsUpPinned(entry);
@@ -359,7 +360,7 @@
* Manager-specific logic that should occur when an entry is added.
* @param headsUpEntry entry added
*/
- void onEntryAdded(HeadsUpEntry headsUpEntry) {
+ protected void onEntryAdded(HeadsUpEntry headsUpEntry) {
NotificationEntry entry = headsUpEntry.mEntry;
entry.setHeadsUp(true);
@@ -391,6 +392,7 @@
entry.demoteStickyHun();
mHeadsUpEntryMap.remove(key);
onEntryRemoved(headsUpEntry);
+ // TODO(b/328390331) move accessibility events to the view layer
entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
if (NotificationsHeadsUpRefactor.isEnabled()) {
headsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
@@ -416,7 +418,16 @@
}
}
- private void updatePinnedMode() {
+ /**
+ * Manager-specific logic, that should occur, when the entry is updated, and its posted time has
+ * changed.
+ *
+ * @param headsUpEntry entry updated
+ */
+ protected void onEntryUpdated(HeadsUpEntry headsUpEntry) {
+ }
+
+ protected void updatePinnedMode() {
boolean hasPinnedNotification = hasPinnedNotificationInternal();
if (hasPinnedNotification == mHasPinnedNotification) {
return;
@@ -471,7 +482,7 @@
@Nullable
protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) {
// TODO(b/315362456) See if callers need to check AvalancheController
- return (HeadsUpEntry) mHeadsUpEntryMap.get(key);
+ return mHeadsUpEntryMap.get(key);
}
/**
@@ -491,7 +502,7 @@
HeadsUpEntry topEntry = null;
for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
if (topEntry == null || entry.compareTo(topEntry) < 0) {
- topEntry = (HeadsUpEntry) entry;
+ topEntry = entry;
}
}
return topEntry;
@@ -720,11 +731,11 @@
updateEntry(true /* updatePostTime */, "setEntry");
}
- public boolean isPinned() {
+ protected boolean isRowPinned() {
return mEntry != null && mEntry.isRowPinned();
}
- public void setPinned(boolean pinned) {
+ protected void setRowPinned(boolean pinned) {
if (mEntry != null) mEntry.setRowPinned(pinned);
}
@@ -764,6 +775,9 @@
return timeLeft;
};
scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)");
+
+ // Notify the manager, that the posted time has changed.
+ onEntryUpdated(this);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
index 420701f..52a2e9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
@@ -196,6 +196,7 @@
* Called when a heads up notification is 'going away' or no longer 'going away'. See
* [HeadsUpManager.setHeadsUpGoingAway].
*/
+ // TODO(b/325936094) delete this callback, and listen to the flow instead
fun onHeadsUpGoingAwayStateChanged(headsUpGoingAway: Boolean)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 0a18eb6..138e1fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -35,9 +35,13 @@
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.fakeConfigurationController
@@ -70,6 +74,7 @@
private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
private val fakeShadeRepository = kosmos.fakeShadeRepository
private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
+ private val headsUpRepository = kosmos.headsUpNotificationRepository
private val zenModeRepository = kosmos.zenModeRepository
val underTest = kosmos.notificationListViewModel
@@ -464,4 +469,120 @@
// THEN footer visibility does not animate
assertThat(shouldShow?.isAnimating).isFalse()
}
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testPinnedHeadsUpRows_filtersForPinnedItems() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+
+ // WHEN there are no pinned rows
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository(key = "0"),
+ fakeHeadsUpRowRepository(key = "1"),
+ fakeHeadsUpRowRepository(key = "2"),
+ )
+ headsUpRepository.setNotifications(
+ rows,
+ )
+ runCurrent()
+
+ // THEN the list is empty
+ assertThat(pinnedHeadsUpRows).isEmpty()
+
+ // WHEN a row gets pinned
+ rows[0].isPinned.value = true
+ runCurrent()
+
+ // THEN it's added to the list
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
+
+ // WHEN more rows are pinned
+ rows[1].isPinned.value = true
+ runCurrent()
+
+ // THEN they are all in the list
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1])
+
+ // WHEN a row gets unpinned
+ rows[0].isPinned.value = false
+ runCurrent()
+
+ // THEN it's removed from the list
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[1])
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testHasPinnedHeadsUpRows_true() =
+ testScope.runTest {
+ val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow)
+
+ headsUpRepository.setNotifications(
+ fakeHeadsUpRowRepository(key = "0", isPinned = true),
+ fakeHeadsUpRowRepository(key = "1")
+ )
+ runCurrent()
+
+ assertThat(hasPinnedHeadsUpRow).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testHasPinnedHeadsUpRows_false() =
+ testScope.runTest {
+ val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow)
+
+ headsUpRepository.setNotifications(
+ fakeHeadsUpRowRepository(key = "0"),
+ fakeHeadsUpRowRepository(key = "1"),
+ )
+ runCurrent()
+
+ assertThat(hasPinnedHeadsUpRow).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testTopHeadsUpRow_emptyList_null() =
+ testScope.runTest {
+ val topHeadsUpRow by collectLastValue(underTest.topHeadsUpRow)
+
+ headsUpRepository.setNotifications(emptyList())
+ runCurrent()
+
+ assertThat(topHeadsUpRow).isNull()
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testHeadsUpAnimationsEnabled_true() =
+ testScope.runTest {
+ val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
+
+ fakeShadeRepository.setQsExpansion(0.0f)
+ fakeKeyguardRepository.setKeyguardShowing(false)
+ runCurrent()
+
+ assertThat(animationsEnabled).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testHeadsUpAnimationsEnabled_keyguardShowing_false() =
+ testScope.runTest {
+ val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
+
+ fakeShadeRepository.setQsExpansion(0.0f)
+ fakeKeyguardRepository.setKeyguardShowing(true)
+ runCurrent()
+
+ assertThat(animationsEnabled).isFalse()
+ }
+
+ private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) =
+ FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
+ this.isPinned.value = isPinned
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt
new file mode 100644
index 0000000..2e983a8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.notification.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeHeadsUpRowRepository(override val key: String, override val elementKey: Any) :
+ HeadsUpRowRepository {
+ override val isPinned: MutableStateFlow<Boolean> = MutableStateFlow(false)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
index 25864ae..165c942 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
@@ -18,11 +18,16 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() }
-class FakeHeadsUpNotificationRepository : HeadsUpNotificationRepository {
- override val hasPinnedHeadsUp = MutableStateFlow(false)
+class FakeHeadsUpNotificationRepository : HeadsUpRepository {
+ override val headsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null)
+ override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> =
+ MutableStateFlow(emptySet())
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
new file mode 100644
index 0000000..9be7dfe
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.notification.stack.data.repository
+
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+
+fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) {
+ setNotifications(*notifications.toTypedArray())
+}
+
+fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) {
+ this.activeHeadsUpRows.value = notifications.toSet()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index 2de26f1..ee3216b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -28,6 +28,7 @@
import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker
import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
+import com.android.systemui.statusbar.notification.ui.viewbinder.headsUpNotificationViewBinder
import com.android.systemui.statusbar.phone.notificationIconAreaController
import java.util.Optional
@@ -37,6 +38,7 @@
backgroundDispatcher = testDispatcher,
configuration = configurationState,
falsingManager = falsingManager,
+ hunBinder = headsUpNotificationViewBinder,
iconAreaController = notificationIconAreaController,
loggerOptional = Optional.of(notificationStatsLogger),
metricsLogger = metricsLogger,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index 930a4bb..c65d0a3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
@@ -25,6 +26,7 @@
import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.notificationShelfViewModel
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.userSetupInteractor
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
@@ -38,6 +40,8 @@
Optional.of(notificationListLoggerViewModel),
activeNotificationsInteractor,
notificationStackInteractor,
+ headsUpNotificationInteractor,
+ keyguardInteractor,
remoteInputInteractor,
seenNotificationsInteractor,
shadeInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt
new file mode 100644
index 0000000..6a995c0
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.notification.ui.viewbinder
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
+
+val Kosmos.headsUpNotificationViewBinder by
+ Kosmos.Fixture { HeadsUpNotificationViewBinder(viewModel = notificationListViewModel) }