diff options
25 files changed, 948 insertions, 95 deletions
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 cc66f8b2f387..f018cc189a5e 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.flag.fakeSceneContainerFlags 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 @@ class SceneContainerStartableTest : SysuiTestCase() { 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 @@ class SceneContainerStartableTest : SysuiTestCase() { 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 000000000000..bba9991883f5 --- /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 9cb920ab0a88..fe4abde22a21 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.footer.shared.FooterViewRefac 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 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void onHeadsUpPinned(NotificationEntry entry) { + if (NotificationsHeadsUpRefactor.isEnabled()) { + return; + } + if (!isKeyguardShowing()) { mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, true); } @@ -4388,6 +4393,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @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 e5e5292d9a94..2b0d2aa6ea2a 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 @@ import dagger.Module ] ) 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 d60ee9896758..000000000000 --- 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 000000000000..ed8c05688a66 --- /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 000000000000..7b40812d55c3 --- /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 5c8f354de485..d1dd7b55c11f 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: 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) + } + } -class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpNotificationRepository) { 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 000000000000..8dc395d2888e --- /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 f2c593d7ffdb..fb528386018b 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 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * @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 8ed1ca28eaf1..7bdd3f99c60d 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.ExpandableView; 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 @@ public class NotificationStackScrollLayoutController implements Dumpable { 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 @@ public class NotificationStackScrollLayoutController implements Dumpable { }); } - 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 6b30393ebc42..97cbbe86389b 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.view.FooterView 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.NotificationStackScroll 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 @@ constructor( 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 @@ constructor( 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 000000000000..ec5e5be44298 --- /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 4744fcbbc7f7..a6ca027d6dbb 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 @@ constructor( val logger: Optional<NotificationLoggerViewModel>, activeNotificationsInteractor: ActiveNotificationsInteractor, notificationStackInteractor: NotificationStackInteractor, + private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, + keyguardInteractor: KeyguardInteractor, remoteInputInteractor: RemoteInputInteractor, seenNotificationsInteractor: SeenNotificationsInteractor, shadeInteractor: ShadeInteractor, @@ -212,4 +218,41 @@ constructor( 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 000000000000..cb360fed77bc --- /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 86bb844e7be3..3f200d578261 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.Context; 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.NotificationEntry; 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.io.PrintWriter; 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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp 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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp if (isExpanded != mIsExpanded) { mIsExpanded = isExpanded; if (isExpanded) { - mHeadsUpGoingAway = false; + mHeadsUpGoingAway.setValue(false); } } } @@ -259,17 +275,17 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp */ @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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp } else { headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)"); } + onEntryUpdated(headsUpEntry); } } @@ -387,11 +404,35 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp } @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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp /////////////////////////////////////////////////////////////////////////////////////////////// // 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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp @Nullable private HeadsUpEntryPhone getTopHeadsUpEntryPhone() { - return (HeadsUpEntryPhone) getTopHeadsUpEntry(); + if (NotificationsHeadsUpRefactor.isEnabled()) { + return (HeadsUpEntryPhone) mTopHeadsUpRow.getValue(); + } else { + return (HeadsUpEntryPhone) getTopHeadsUpEntry(); + } } @Override @@ -439,12 +490,32 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp 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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp } @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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp 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 6f7e0468c246..20a82a403eb7 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { // 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { // 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { * 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } } - 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { @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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { 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 420701f026d2..52a2e9ccc163 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 @@ interface OnHeadsUpPhoneListenerChange { * 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 0a18eb66c4df..138e1fa5c29c 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.power.shared.model.WakefulnessState 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 @@ class NotificationListViewModelTest : SysuiTestCase() { 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 @@ class NotificationListViewModelTest : SysuiTestCase() { // 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 000000000000..2e983a820240 --- /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 25864aee2136..165c9429c917 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 @@ package com.android.systemui.statusbar.notification.stack.data.repository 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 000000000000..9be7dfe9a1a9 --- /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 2de26f13ad73..ee3216b2243d 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.notificationActivityStarter 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 @@ val Kosmos.notificationListViewBinder by Fixture { 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 930a4bbb2daa..c65d0a33cf67 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.activeNotif 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 @@ val Kosmos.notificationListViewModel by Fixture { 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 000000000000..6a995c08ecae --- /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) } |