summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt510
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt295
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt99
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt129
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt137
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt14
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt42
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt (renamed from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt)16
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt19
22 files changed, 1096 insertions, 267 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 a120bdc0b743..540a85aeb695 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
@@ -175,12 +175,14 @@ class SceneContainerStartableTest : SysuiTestCase() {
transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone)
assertThat(isVisible).isFalse()
- kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+ kosmos.headsUpNotificationRepository.setNotifications(
buildNotificationRows(isPinned = true)
+ )
assertThat(isVisible).isTrue()
- kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+ kosmos.headsUpNotificationRepository.setNotifications(
buildNotificationRows(isPinned = false)
+ )
assertThat(isVisible).isFalse()
}
@@ -1699,8 +1701,8 @@ class SceneContainerStartableTest : SysuiTestCase() {
return transitionStateFlow
}
- private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> =
- setOf(
+ private fun buildNotificationRows(isPinned: Boolean = false): List<HeadsUpRowRepository> =
+ listOf(
fakeHeadsUpRowRepository(key = "0", isPinned = isPinned),
fakeHeadsUpRowRepository(key = "1", isPinned = isPinned),
fakeHeadsUpRowRepository(key = "2", isPinned = isPinned),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
new file mode 100644
index 000000000000..8810ade1d851
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -0,0 +1,510 @@
+/*
+ * 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.collection.coordinator
+
+import android.app.Notification
+import android.app.NotificationManager.IMPORTANCE_DEFAULT
+import android.app.NotificationManager.IMPORTANCE_LOW
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.modifyEntry
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.google.common.truth.StringSubject
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
+
+ private val kosmos =
+ testKosmos().apply {
+ testDispatcher = UnconfinedTestDispatcher()
+ statusBarStateController =
+ mock<SysuiStatusBarStateController>().also { mock ->
+ doAnswer { statusBarState.ordinal }.whenever(mock).state
+ }
+ fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+ }
+ private val notifPipeline: NotifPipeline = mock()
+ private var statusBarState: StatusBarState = StatusBarState.KEYGUARD
+
+ @Test
+ fun topUnseenSectioner() {
+ val solo = NotificationEntryBuilder().setTag("solo").build()
+ val child1 = NotificationEntryBuilder().setTag("child1").build()
+ val child2 = NotificationEntryBuilder().setTag("child2").build()
+ val parent = NotificationEntryBuilder().setTag("parent").build()
+ val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+ runCoordinatorTest {
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+ assertThat(topUnseenSectioner.isInSection(solo)).isTrue()
+ assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child1.key
+ assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child1)).isTrue()
+ assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = parent.key
+ assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(parent)).isTrue()
+ assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+ assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+ assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+ }
+ }
+
+ @Test
+ fun topOngoingSectioner() {
+ val solo = NotificationEntryBuilder().setTag("solo").build()
+ val child1 = NotificationEntryBuilder().setTag("child1").build()
+ val child2 = NotificationEntryBuilder().setTag("child2").build()
+ val parent = NotificationEntryBuilder().setTag("parent").build()
+ val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+ runCoordinatorTest {
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+ assertThat(topOngoingSectioner.isInSection(solo)).isTrue()
+ assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+ assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child1)).isTrue()
+ assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = parent.key
+ assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(parent)).isTrue()
+ assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+ assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+ assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+ }
+ }
+
+ @Test
+ fun testPromoter() {
+ val child1 = NotificationEntryBuilder().setTag("child1").build()
+ val child2 = NotificationEntryBuilder().setTag("child2").build()
+ val child3 = NotificationEntryBuilder().setTag("child3").build()
+ val parent = NotificationEntryBuilder().setTag("parent").build()
+ GroupEntryBuilder()
+ .addChild(child1)
+ .addChild(child2)
+ .addChild(child3)
+ .setSummary(parent)
+ .build()
+
+ runCoordinatorTest {
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+ assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+ assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+ assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+ assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(child2))
+ .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+ assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+ assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+ assertThat(promoter.shouldPromoteToTopLevel(child2))
+ .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+ assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+ assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+ }
+ }
+
+ @Test
+ fun topOngoingIdentifier() {
+ val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+ val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+ val parent = defaultEntryBuilder().setTag("parent").setRank(3).build()
+ val child1 = defaultEntryBuilder().setTag("child1").setRank(4).build()
+ val child2 = defaultEntryBuilder().setTag("child2").setRank(5).build()
+ val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+ val listEntryList = listOf(group, solo1, solo2)
+
+ runCoordinatorTest {
+ // TEST: base case - no entries in the list
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: none of these are unseen or ongoing yet, so don't pick them
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: when solo2 is the only one colorized, it gets picked up
+ solo2.setColorizedFgs(true)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo2.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: once solo1 is colorized, it takes priority for being ranked higher
+ solo1.setColorizedFgs(true)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo1.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: changing just the rank of solo1 causes it to pick up solo2 instead
+ solo1.modifyEntry { setRank(20) }
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo2.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: switching to SHADE disables the whole thing
+ statusBarState = StatusBarState.SHADE
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: switching back to KEYGUARD picks up the same entry again
+ statusBarState = StatusBarState.KEYGUARD
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo2.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: updating to not colorized revokes the top-ongoing status
+ solo2.setColorizedFgs(false)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo1.key)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: updating the importance to LOW revokes top-ongoing status
+ solo1.modifyEntry { setImportance(IMPORTANCE_LOW) }
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+ }
+ }
+
+ @Test
+ fun topUnseenIdentifier() {
+ val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+ val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+ val parent = defaultEntryBuilder().setTag("parent").setRank(4).build()
+ val child1 = defaultEntryBuilder().setTag("child1").setRank(5).build()
+ val child2 = defaultEntryBuilder().setTag("child2").setRank(6).build()
+ val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+ val listEntryList = listOf(group, solo1, solo2)
+ val notificationEntryList = listOf(solo1, solo2, parent, child1, child2)
+
+ runCoordinatorTest {
+ // All entries are added (and now unseen)
+ notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+ // TEST: Filtered out entries are ignored
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: top-ranked unseen child is selected (not the summary)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(group))
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(child1.key)
+
+ // TEST: top-ranked entry is picked
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen
+ solo1.setColorizedFgs(true)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(solo1.key)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: no more colorized entries
+ solo1.setColorizedFgs(false)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: if the rank of solo1 is reduced, solo2 will be preferred
+ solo1.modifyEntry { setRank(3) }
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: switching to SHADE state will disable the entire selector
+ statusBarState = StatusBarState.SHADE
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: switching back to KEYGUARD re-enables the selector
+ statusBarState = StatusBarState.KEYGUARD
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: QS Expansion does not mark entries as seen
+ setShadeAndQsExpansionThenWait(0f, 1f)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: Shade expansion does mark entries as seen
+ setShadeAndQsExpansionThenWait(1f, 0f)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: Entries updated while shade is expanded are NOT marked unseen
+ collectionListener.onEntryUpdated(solo1)
+ collectionListener.onEntryUpdated(solo2)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
+ // TEST: Entries updated after shade is collapsed ARE marked unseen
+ setShadeAndQsExpansionThenWait(0f, 0f)
+ collectionListener.onEntryUpdated(solo1)
+ collectionListener.onEntryUpdated(solo2)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: low importance disqualifies the entry for top unseen
+ solo2.modifyEntry { setImportance(IMPORTANCE_LOW) }
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+ }
+ }
+
+ @Test
+ fun topUnseenIdentifier_headsUpMarksSeen() {
+ val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+ val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+ val listEntryList = listOf(solo1, solo2)
+ val notificationEntryList = listOf(solo1, solo2)
+
+ val hunRepo1 = solo1.fakeHeadsUpRowRepository()
+ val hunRepo2 = solo2.fakeHeadsUpRowRepository()
+
+ runCoordinatorTest {
+ // All entries are added (and now unseen)
+ notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+ // TEST: top-ranked entry is picked
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: heads up state and waiting isn't enough to be seen
+ kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value =
+ listOf(hunRepo1, hunRepo2)
+ testScheduler.advanceTimeBy(1.seconds)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: even being pinned doesn't take effect immediately
+ hunRepo1.isPinned.value = true
+ testScheduler.advanceTimeBy(0.5.seconds)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+ // TEST: after being pinned a full second, solo1 is seen
+ testScheduler.advanceTimeBy(0.5.seconds)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+ // TEST: repeat; being heads up and pinned for 1 second triggers seen
+ kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value = listOf(hunRepo2)
+ hunRepo1.isPinned.value = false
+ hunRepo2.isPinned.value = true
+ testScheduler.advanceTimeBy(1.seconds)
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+ assertThatTopUnseenKey().isEqualTo(null)
+ }
+ }
+
+ private fun NotificationEntry.fakeHeadsUpRowRepository() =
+ FakeHeadsUpRowRepository(key = key, elementKey = Any())
+
+ private fun KeyguardCoordinatorTestScope.setShadeAndQsExpansionThenWait(
+ shadeExpansion: Float,
+ qsExpansion: Float
+ ) {
+ kosmos.shadeTestUtil.setShadeAndQsExpansion(shadeExpansion, qsExpansion)
+ // The coordinator waits a fraction of a second for the shade expansion to stick.
+ testScheduler.advanceTimeBy(1.seconds)
+ }
+
+ private fun defaultEntryBuilder() = NotificationEntryBuilder().setImportance(IMPORTANCE_DEFAULT)
+
+ private fun runCoordinatorTest(testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit) {
+ kosmos.lockScreenMinimalismCoordinator.attach(notifPipeline)
+ kosmos.testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
+ KeyguardCoordinatorTestScope(
+ kosmos.lockScreenMinimalismCoordinator,
+ kosmos.testScope,
+ kosmos.fakeSettings,
+ )
+ .testBlock()
+ }
+ }
+
+ private inner class KeyguardCoordinatorTestScope(
+ private val coordinator: LockScreenMinimalismCoordinator,
+ private val scope: TestScope,
+ private val fakeSettings: FakeSettings,
+ ) : CoroutineScope by scope {
+ fun assertThatTopOngoingKey(): StringSubject {
+ return assertThat(
+ kosmos.activeNotificationListRepository.topOngoingNotificationKey.value
+ )
+ }
+
+ fun assertThatTopUnseenKey(): StringSubject {
+ return assertThat(
+ kosmos.activeNotificationListRepository.topUnseenNotificationKey.value
+ )
+ }
+
+ val testScheduler: TestCoroutineScheduler
+ get() = scope.testScheduler
+
+ val promoter: NotifPromoter
+ get() = coordinator.unseenNotifPromoter
+
+ val topUnseenSectioner: NotifSectioner
+ get() = coordinator.topUnseenSectioner
+
+ val topOngoingSectioner: NotifSectioner
+ get() = coordinator.topOngoingSectioner
+
+ val onBeforeTransformGroupsListener: OnBeforeTransformGroupsListener =
+ argumentCaptor { verify(notifPipeline).addOnBeforeTransformGroupsListener(capture()) }
+ .lastValue
+
+ val collectionListener: NotifCollectionListener =
+ argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue
+
+ var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
+ get() =
+ fakeSettings.getIntForUser(
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ UserHandle.USER_CURRENT,
+ ) == 1
+ set(value) {
+ fakeSettings.putIntForUser(
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ if (value) 1 else 2,
+ UserHandle.USER_CURRENT,
+ )
+ }
+ }
+
+ companion object {
+
+ private fun NotificationEntry.setColorizedFgs(colorized: Boolean) {
+ sbn.notification.setColorizedFgs(colorized)
+ }
+
+ private fun Notification.setColorizedFgs(colorized: Boolean) {
+ extras.putBoolean(Notification.EXTRA_COLORIZED, colorized)
+ flags =
+ if (colorized) {
+ flags or Notification.FLAG_FOREGROUND_SERVICE
+ } else {
+ flags and Notification.FLAG_FOREGROUND_SERVICE.inv()
+ }
+ }
+ }
+}
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
index 8b4265f552fe..14134ccc34d0 100644
--- 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
@@ -33,7 +33,6 @@ import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRo
import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
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
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index f8e633731c42..f96cf1011fb8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -43,7 +43,6 @@ import com.android.systemui.statusbar.notification.data.repository.setActiveNoti
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
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 1ba274ff4e76..0e06117b2693 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -37,6 +37,8 @@ import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefac
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import javax.inject.Inject
@@ -55,6 +57,7 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha
FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
+ NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index 3dcaff3de35a..b342722ebb09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -52,8 +52,11 @@ constructor(val proxy: DeviceConfigProxy, val context: Context) {
}
fun getNotificationBuckets(): IntArray {
- if (PriorityPeopleSection.isEnabled || NotificationMinimalismPrototype.V2.isEnabled
- || NotificationClassificationFlag.isEnabled) {
+ if (
+ PriorityPeopleSection.isEnabled ||
+ NotificationMinimalismPrototype.isEnabled ||
+ NotificationClassificationFlag.isEnabled
+ ) {
// We don't need this list to be adaptive, it can be the superset of all features.
return PriorityBucket.getAllInOrder()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
new file mode 100644
index 000000000000..a6605f652ff3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -0,0 +1,295 @@
+/*
+ * 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.collection.coordinator
+
+import android.annotation.SuppressLint
+import android.app.NotificationManager
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+/**
+ * If the setting is enabled, this will track seen notifications and ensure that they only show in
+ * the shelf on the lockscreen.
+ *
+ * This class is a replacement of the [OriginalUnseenKeyguardCoordinator].
+ */
+@CoordinatorScope
+@SuppressLint("SharedFlowCreation")
+class LockScreenMinimalismCoordinator
+@Inject
+constructor(
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val dumpManager: DumpManager,
+ private val headsUpInteractor: HeadsUpNotificationInteractor,
+ private val logger: LockScreenMinimalismCoordinatorLogger,
+ @Application private val scope: CoroutineScope,
+ private val secureSettings: SecureSettings,
+ private val seenNotificationsInteractor: SeenNotificationsInteractor,
+ private val statusBarStateController: StatusBarStateController,
+ private val shadeInteractor: ShadeInteractor,
+) : Coordinator, Dumpable {
+
+ private val unseenNotifications = mutableSetOf<NotificationEntry>()
+ private var isShadeVisible = false
+ private var unseenFilterEnabled = false
+
+ override fun attach(pipeline: NotifPipeline) {
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) {
+ return
+ }
+ pipeline.addPromoter(unseenNotifPromoter)
+ pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
+ pipeline.addCollectionListener(collectionListener)
+ scope.launch { trackUnseenFilterSettingChanges() }
+ dumpManager.registerDumpable(this)
+ }
+
+ private suspend fun trackSeenNotifications() {
+ coroutineScope {
+ launch { clearUnseenNotificationsWhenShadeIsExpanded() }
+ launch { markHeadsUpNotificationsAsSeen() }
+ }
+ }
+
+ private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
+ shadeInteractor.isShadeFullyExpanded.collectLatest { isExpanded ->
+ // Give keyguard events time to propagate, in case this expansion is part of the
+ // keyguard transition and not the user expanding the shade
+ delay(SHADE_VISIBLE_SEEN_TIMEOUT)
+ isShadeVisible = isExpanded
+ if (isExpanded) {
+ logger.logShadeVisible(unseenNotifications.size)
+ unseenNotifications.clear()
+ // no need to invalidateList; filtering is inactive while shade is open
+ } else {
+ logger.logShadeHidden()
+ }
+ }
+ }
+
+ private suspend fun markHeadsUpNotificationsAsSeen() {
+ headsUpInteractor.topHeadsUpRowIfPinned
+ .map { it?.let { headsUpInteractor.notificationKey(it) } }
+ .collectLatest { key ->
+ if (key == null) {
+ logger.logTopHeadsUpRow(key = null, wasUnseenWhenPinned = false)
+ } else {
+ val wasUnseenWhenPinned = unseenNotifications.any { it.key == key }
+ logger.logTopHeadsUpRow(key, wasUnseenWhenPinned)
+ if (wasUnseenWhenPinned) {
+ delay(HEADS_UP_SEEN_TIMEOUT)
+ val wasUnseenAfterDelay = unseenNotifications.removeIf { it.key == key }
+ logger.logHunHasBeenSeen(key, wasUnseenAfterDelay)
+ // no need to invalidateList; nothing should change until after heads up
+ }
+ }
+ }
+ }
+
+ private fun unseenFeatureEnabled(): Flow<Boolean> {
+ // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here?
+ // Or should we actually just repurpose using the existing setting?
+ if (NotificationMinimalismPrototype.isEnabled) {
+ return flowOf(true)
+ }
+ return secureSettings
+ // emit whenever the setting has changed
+ .observerFlow(
+ UserHandle.USER_ALL,
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ )
+ // perform a query immediately
+ .onStart { emit(Unit) }
+ // for each change, lookup the new value
+ .map {
+ secureSettings.getIntForUser(
+ name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ def = 0,
+ userHandle = UserHandle.USER_CURRENT,
+ ) == 1
+ }
+ // don't emit anything if nothing has changed
+ .distinctUntilChanged()
+ // perform lookups on the bg thread pool
+ .flowOn(bgDispatcher)
+ // only track the most recent emission, if events are happening faster than they can be
+ // consumed
+ .conflate()
+ }
+
+ private suspend fun trackUnseenFilterSettingChanges() {
+ unseenFeatureEnabled().collectLatest { isSettingEnabled ->
+ // update local field and invalidate if necessary
+ if (isSettingEnabled != unseenFilterEnabled) {
+ unseenFilterEnabled = isSettingEnabled
+ unseenNotifPromoter.invalidateList("unseen setting changed")
+ }
+ // if the setting is enabled, then start tracking and filtering unseen notifications
+ logger.logTrackingUnseen(isSettingEnabled)
+ if (isSettingEnabled) {
+ trackSeenNotifications()
+ }
+ }
+ }
+
+ private val collectionListener =
+ object : NotifCollectionListener {
+ override fun onEntryAdded(entry: NotificationEntry) {
+ if (!isShadeVisible) {
+ logger.logUnseenAdded(entry.key)
+ unseenNotifications.add(entry)
+ }
+ }
+
+ override fun onEntryUpdated(entry: NotificationEntry) {
+ if (!isShadeVisible) {
+ logger.logUnseenUpdated(entry.key)
+ unseenNotifications.add(entry)
+ }
+ }
+
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ if (unseenNotifications.remove(entry)) {
+ logger.logUnseenRemoved(entry.key)
+ }
+ }
+ }
+
+ private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+ // Only ever elevate a top unseen notification on keyguard, not even locked shade
+ if (statusBarStateController.state != StatusBarState.KEYGUARD) {
+ seenNotificationsInteractor.setTopOngoingNotification(null)
+ seenNotificationsInteractor.setTopUnseenNotification(null)
+ return
+ }
+ // On keyguard pick the top-ranked unseen or ongoing notification to elevate
+ val nonSummaryEntries: Sequence<NotificationEntry> =
+ list
+ .asSequence()
+ .flatMap {
+ when (it) {
+ is NotificationEntry -> listOfNotNull(it)
+ is GroupEntry -> it.children
+ else -> error("unhandled type of $it")
+ }
+ }
+ .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
+ seenNotificationsInteractor.setTopOngoingNotification(
+ nonSummaryEntries
+ .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
+ .minByOrNull { it.ranking.rank }
+ )
+ seenNotificationsInteractor.setTopUnseenNotification(
+ nonSummaryEntries
+ .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
+ .minByOrNull { it.ranking.rank }
+ )
+ }
+
+ @VisibleForTesting
+ val unseenNotifPromoter =
+ object : NotifPromoter(TAG) {
+ override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
+ when {
+ NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false
+ seenNotificationsInteractor.isTopOngoingNotification(child) -> true
+ !NotificationMinimalismPrototype.ungroupTopUnseen -> false
+ else -> seenNotificationsInteractor.isTopUnseenNotification(child)
+ }
+ }
+
+ val topOngoingSectioner =
+ object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
+ override fun isInSection(entry: ListEntry): Boolean {
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+ return entry.anyEntry { notificationEntry ->
+ seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
+ }
+ }
+ }
+
+ val topUnseenSectioner =
+ object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
+ override fun isInSection(entry: ListEntry): Boolean {
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+ return entry.anyEntry { notificationEntry ->
+ seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
+ }
+ }
+ }
+
+ private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
+ when {
+ predicate(representativeEntry) -> true
+ this !is GroupEntry -> false
+ else -> children.any(predicate)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) =
+ with(pw.asIndenting()) {
+ seenNotificationsInteractor.dump(this)
+ printCollection("unseen notifications", unseenNotifications) { println(it.key) }
+ }
+
+ companion object {
+ private const val TAG = "LockScreenMinimalismCoordinator"
+ private val SHADE_VISIBLE_SEEN_TIMEOUT = 0.25.seconds
+ private val HEADS_UP_SEEN_TIMEOUT = 0.75.seconds
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
new file mode 100644
index 000000000000..e44a77c30999
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.collection.coordinator
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.UnseenNotificationLog
+import javax.inject.Inject
+
+private const val TAG = "LockScreenMinimalismCoordinator"
+
+class LockScreenMinimalismCoordinatorLogger
+@Inject
+constructor(
+ @UnseenNotificationLog private val buffer: LogBuffer,
+) {
+
+ fun logTrackingUnseen(trackingUnseen: Boolean) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { bool1 = trackingUnseen },
+ messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
+ )
+
+ fun logShadeVisible(numUnseen: Int) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { int1 = numUnseen },
+ messagePrinter = { "Shade expanded. Notifications marked as seen: $int1" }
+ )
+ }
+
+ fun logShadeHidden() {
+ buffer.log(TAG, LogLevel.DEBUG, "Shade no longer expanded.")
+ }
+
+ fun logUnseenAdded(key: String) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { str1 = key },
+ messagePrinter = { "Unseen notif added: $str1" },
+ )
+
+ fun logUnseenUpdated(key: String) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { str1 = key },
+ messagePrinter = { "Unseen notif updated: $str1" },
+ )
+
+ fun logUnseenRemoved(key: String) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = { str1 = key },
+ messagePrinter = { "Unseen notif removed: $str1" },
+ )
+
+ fun logHunHasBeenSeen(key: String, wasUnseen: Boolean) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = {
+ str1 = key
+ bool1 = wasUnseen
+ },
+ messagePrinter = { "Heads up notif has been seen: $str1 wasUnseen=$bool1" },
+ )
+
+ fun logTopHeadsUpRow(key: String?, wasUnseenWhenPinned: Boolean) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ messageInitializer = {
+ str1 = key
+ bool1 = wasUnseenWhenPinned
+ },
+ messagePrinter = { "New notif is top heads up: $str1 wasUnseen=$bool1" },
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 99327d1fe116..73ce48b2324a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -47,6 +47,7 @@ constructor(
hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
keyguardCoordinator: KeyguardCoordinator,
unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator,
+ lockScreenMinimalismCoordinator: LockScreenMinimalismCoordinator,
rankingCoordinator: RankingCoordinator,
colorizedFgsCoordinator: ColorizedFgsCoordinator,
deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
@@ -87,7 +88,11 @@ constructor(
mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
mCoordinators.add(hideNotifsForOtherUsersCoordinator)
mCoordinators.add(keyguardCoordinator)
- mCoordinators.add(unseenKeyguardCoordinator)
+ if (NotificationMinimalismPrototype.isEnabled) {
+ mCoordinators.add(lockScreenMinimalismCoordinator)
+ } else {
+ mCoordinators.add(unseenKeyguardCoordinator)
+ }
mCoordinators.add(rankingCoordinator)
mCoordinators.add(colorizedFgsCoordinator)
mCoordinators.add(deviceProvisionedCoordinator)
@@ -120,12 +125,12 @@ constructor(
}
// Manually add Ordered Sections
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- mOrderedSections.add(unseenKeyguardCoordinator.topOngoingSectioner) // Top Ongoing
+ if (NotificationMinimalismPrototype.isEnabled) {
+ mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
}
mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- mOrderedSections.add(unseenKeyguardCoordinator.topUnseenSectioner) // Top Unseen
+ if (NotificationMinimalismPrototype.isEnabled) {
+ mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
}
mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
if (PriorityPeopleSection.isEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
index 5dd1663f712f..5b25b117c761 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.notification.collection.coordinator
import android.annotation.SuppressLint
-import android.app.NotificationManager
import android.os.UserHandle
import android.provider.Settings
import androidx.annotation.VisibleForTesting
@@ -30,21 +29,14 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.expansionChanges
-import com.android.systemui.statusbar.notification.collection.GroupEntry
-import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.headsUpEvents
import com.android.systemui.util.asIndenting
@@ -73,9 +65,12 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
/**
- * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
- * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the
- * lockscreen.
+ * If the setting is enabled, this will track and hide seen notifications on the lockscreen.
+ *
+ * This is the "original" unseen keyguard coordinator because this is the logic originally developed
+ * for large screen devices where showing "seen" notifications on the lock screen was distracting.
+ * Moreover, this file was created during a project that will replace this logic, so the
+ * [LockScreenMinimalismCoordinator] is the expected replacement of this file.
*/
@CoordinatorScope
@SuppressLint("SharedFlowCreation")
@@ -100,10 +95,7 @@ constructor(
private var unseenFilterEnabled = false
override fun attach(pipeline: NotifPipeline) {
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- pipeline.addPromoter(unseenNotifPromoter)
- pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
- }
+ NotificationMinimalismPrototype.assertInLegacyMode()
pipeline.addFinalizeFilter(unseenNotifFilter)
pipeline.addCollectionListener(collectionListener)
scope.launch { trackUnseenFilterSettingChanges() }
@@ -112,6 +104,7 @@ constructor(
private suspend fun trackSeenNotifications() {
// Whether or not keyguard is visible (or occluded).
+ @Suppress("DEPRECATION")
val isKeyguardPresentFlow: Flow<Boolean> =
keyguardTransitionInteractor
.transitionValue(
@@ -265,11 +258,9 @@ constructor(
}
private fun unseenFeatureEnabled(): Flow<Boolean> {
- if (
- NotificationMinimalismPrototype.V1.isEnabled ||
- NotificationMinimalismPrototype.V2.isEnabled
- ) {
- return flowOf(true)
+ if (NotificationMinimalismPrototype.isEnabled) {
+ // TODO(b/330387368): should this really just be turned off? If so, hide the setting.
+ return flowOf(false)
}
return secureSettings
// emit whenever the setting has changed
@@ -340,110 +331,18 @@ constructor(
}
}
- private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
- // Only ever elevate a top unseen notification on keyguard, not even locked shade
- if (statusBarStateController.state != StatusBarState.KEYGUARD) {
- seenNotificationsInteractor.setTopOngoingNotification(null)
- seenNotificationsInteractor.setTopUnseenNotification(null)
- return
- }
- // On keyguard pick the top-ranked unseen or ongoing notification to elevate
- val nonSummaryEntries: Sequence<NotificationEntry> =
- list
- .asSequence()
- .flatMap {
- when (it) {
- is NotificationEntry -> listOfNotNull(it)
- is GroupEntry -> it.children
- else -> error("unhandled type of $it")
- }
- }
- .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
- seenNotificationsInteractor.setTopOngoingNotification(
- nonSummaryEntries
- .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
- .minByOrNull { it.ranking.rank }
- )
- seenNotificationsInteractor.setTopUnseenNotification(
- nonSummaryEntries
- .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
- .minByOrNull { it.ranking.rank }
- )
- }
-
- @VisibleForTesting
- val unseenNotifPromoter =
- object : NotifPromoter("$TAG-unseen") {
- override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
- else if (!NotificationMinimalismPrototype.V2.ungroupTopUnseen) false
- else
- seenNotificationsInteractor.isTopOngoingNotification(child) ||
- seenNotificationsInteractor.isTopUnseenNotification(child)
- }
-
- val topOngoingSectioner =
- object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
- override fun isInSection(entry: ListEntry): Boolean {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
- return entry.anyEntry { notificationEntry ->
- seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
- }
- }
- }
-
- val topUnseenSectioner =
- object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
- override fun isInSection(entry: ListEntry): Boolean {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
- return entry.anyEntry { notificationEntry ->
- seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
- }
- }
- }
-
- private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
- when {
- predicate(representativeEntry) -> true
- this !is GroupEntry -> false
- else -> children.any(predicate)
- }
-
@VisibleForTesting
val unseenNotifFilter =
- object : NotifFilter("$TAG-unseen") {
+ object : NotifFilter(TAG) {
var hasFilteredAnyNotifs = false
- /**
- * Encapsulates a definition of "being on the keyguard". Note that these two definitions
- * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does
- * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing]
- * is any state where the keyguard has not been dismissed, including locked shade and
- * occluded lock screen.
- *
- * Returning false for locked shade and occluded states means that this filter will
- * allow seen notifications to appear in the locked shade.
- */
- private fun isOnKeyguard(): Boolean =
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- false // disable this feature under this prototype
- } else if (
- NotificationMinimalismPrototype.V1.isEnabled &&
- NotificationMinimalismPrototype.V1.showOnLockedShade
- ) {
- statusBarStateController.state == StatusBarState.KEYGUARD
- } else {
- keyguardRepository.isKeyguardShowing()
- }
-
override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
when {
// Don't apply filter if the setting is disabled
!unseenFilterEnabled -> false
// Don't apply filter if the keyguard isn't currently showing
- !isOnKeyguard() -> false
+ !keyguardRepository.isKeyguardShowing() -> false
// Don't apply the filter if the notification is unseen
unseenNotifications.contains(entry) -> false
// Don't apply the filter to (non-promoted) group summaries
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index caa6c17ac3d2..71c98b8a47f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -150,8 +150,9 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable {
if (entry == null) {
return false;
}
- boolean isTopUnseen = NotificationMinimalismPrototype.V2.isEnabled()
- && mSeenNotificationsInteractor.isTopUnseenNotification(entry);
+ boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled()
+ && (mSeenNotificationsInteractor.isTopUnseenNotification(entry)
+ || mSeenNotificationsInteractor.isTopOngoingNotification(entry));
if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
return !mVisibilityLocationProvider.isInVisibleLocation(entry);
}
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 bf44b9f3cf78..24b75d49ed16 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
@@ -30,6 +30,7 @@ import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -44,8 +45,17 @@ constructor(
private val shadeInteractor: ShadeInteractor,
) {
+ /** The top-ranked heads up row, regardless of pinned state */
val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow
+ /** The top-ranked heads up row, if that row is pinned */
+ val topHeadsUpRowIfPinned: Flow<HeadsUpRowKey?> =
+ headsUpRepository.topHeadsUpRow
+ .flatMapLatest { repository ->
+ repository?.isPinned?.map { pinned -> repository.takeIf { pinned } } ?: flowOf(null)
+ }
+ .distinctUntilChanged()
+
/** Set of currently pinned top-level heads up rows to be displayed. */
val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
@@ -89,10 +99,10 @@ constructor(
flowOf(false)
} else {
combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
- hasPinnedRows,
- animatingAway ->
- hasPinnedRows || animatingAway
- }
+ hasPinnedRows,
+ animatingAway ->
+ hasPinnedRows || animatingAway
+ }
}
}
@@ -127,6 +137,9 @@ constructor(
fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey
+ /** Returns the Notification Key (the standard string) of this row. */
+ fun notificationKey(key: HeadsUpRowKey): String = (key as HeadsUpRowRepository).key
+
fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
headsUpRepository.setHeadsUpAnimatingAway(animatingAway)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 85c66bd6f25a..948a3c2f65b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -16,10 +16,12 @@
package com.android.systemui.statusbar.notification.domain.interactor
+import android.util.IndentingPrintWriter
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.util.printSection
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
@@ -41,24 +43,42 @@ constructor(
/** Set the entry that is identified as the top ongoing notification. */
fun setTopOngoingNotification(entry: NotificationEntry?) {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
notificationListRepository.topOngoingNotificationKey.value = entry?.key
}
/** Determine if the given notification is the top ongoing notification. */
fun isTopOngoingNotification(entry: NotificationEntry?): Boolean =
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
else
entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key
/** Set the entry that is identified as the top unseen notification. */
fun setTopUnseenNotification(entry: NotificationEntry?) {
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
notificationListRepository.topUnseenNotificationKey.value = entry?.key
}
/** Determine if the given notification is the top unseen notification. */
fun isTopUnseenNotification(entry: NotificationEntry?): Boolean =
- if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+ if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key
+
+ fun dump(pw: IndentingPrintWriter) =
+ with(pw) {
+ printSection("SeenNotificationsInteractor") {
+ print(
+ "hasFilteredOutSeenNotifications",
+ notificationListRepository.hasFilteredOutSeenNotifications.value
+ )
+ print(
+ "topOngoingNotificationKey",
+ notificationListRepository.topOngoingNotificationKey.value
+ )
+ print(
+ "topUnseenNotificationKey",
+ notificationListRepository.topUnseenNotificationKey.value
+ )
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
index bf37036ee018..06f3db504aaf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
@@ -24,102 +24,43 @@ import com.android.systemui.flags.RefactorFlagUtils
/** Helper for reading or using the minimalism prototype flag state. */
@Suppress("NOTHING_TO_INLINE")
object NotificationMinimalismPrototype {
-
- val version: Int by lazy {
- SystemProperties.getInt("persist.notification_minimalism_prototype.version", 2)
- }
-
- object V1 {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the heads-up cycling animation enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.notificationMinimalismPrototype() && version == 1
-
- /**
- * the prototype will now show seen notifications on the locked shade by default, but this
- * property read allows that to be quickly disabled for testing
- */
- val showOnLockedShade: Boolean
- get() =
- if (isUnexpectedlyInLegacyMode()) false
- else
- SystemProperties.getBoolean(
- "persist.notification_minimalism_prototype.show_on_locked_shade",
- true
- )
-
- /** gets the configurable max number of notifications */
- val maxNotifs: Int
- get() =
- if (isUnexpectedlyInLegacyMode()) -1
- else
- SystemProperties.getInt(
- "persist.notification_minimalism_prototype.lock_screen_max_notifs",
- 1
- )
-
- /**
- * Called to ensure code is only run when the flag is enabled. This protects users from the
- * unintended behaviors caused by accidentally running new logic, while also crashing on an
- * eng build to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun isUnexpectedlyInLegacyMode() =
- RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
- /**
- * Called to ensure code is only run when the flag is disabled. This will throw an exception
- * if the flag is enabled to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
- }
- object V2 {
- const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the heads-up cycling animation enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.notificationMinimalismPrototype() && version == 2
-
- /**
- * The prototype will (by default) use a promoter to ensure that the top unseen notification
- * is not grouped, but this property read allows that behavior to be disabled.
- */
- val ungroupTopUnseen: Boolean
- get() =
- if (isUnexpectedlyInLegacyMode()) false
- else
- SystemProperties.getBoolean(
- "persist.notification_minimalism_prototype.ungroup_top_unseen",
- true
- )
-
- /**
- * Called to ensure code is only run when the flag is enabled. This protects users from the
- * unintended behaviors caused by accidentally running new logic, while also crashing on an
- * eng build to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun isUnexpectedlyInLegacyMode() =
- RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
- /**
- * Called to ensure code is only run when the flag is disabled. This will throw an exception
- * if the flag is enabled to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
- }
+ const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the heads-up cycling animation enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.notificationMinimalismPrototype()
+
+ /**
+ * The prototype will (by default) use a promoter to ensure that the top unseen notification is
+ * not grouped, but this property read allows that behavior to be disabled.
+ */
+ val ungroupTopUnseen: Boolean
+ get() =
+ if (isUnexpectedlyInLegacyMode()) false
+ else
+ SystemProperties.getBoolean(
+ "persist.notification_minimalism_prototype.ungroup_top_unseen",
+ false
+ )
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 391bc43a784c..06222fdb2761 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -74,7 +74,7 @@ constructor(
/** Whether we allow keyguard to show less important notifications above the shelf. */
private val limitLockScreenToOneImportant
- get() = NotificationMinimalismPrototype.V2.isEnabled
+ get() = NotificationMinimalismPrototype.isEnabled
/** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
private var dividerHeight by notNull<Float>()
@@ -405,16 +405,8 @@ constructor(
fun updateResources() {
maxKeyguardNotifications =
- infiniteIfNegative(
- if (NotificationMinimalismPrototype.V1.isEnabled) {
- NotificationMinimalismPrototype.V1.maxNotifs
- } else {
- resources.getInteger(R.integer.keyguard_max_notification_count)
- }
- )
- maxNotificationsExcludesMedia =
- NotificationMinimalismPrototype.V1.isEnabled ||
- NotificationMinimalismPrototype.V2.isEnabled
+ infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+ maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled
dividerHeight =
max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 52af907c7b7d..64eadb7db1e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -36,7 +36,6 @@ import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
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.setNotifications
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index acb005f6e72f..0407fc14d35a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -42,7 +42,7 @@ import org.mockito.quality.Strictness
@RunWith(AndroidJUnit4::class)
@SmallTest
// this class has no testable logic with either of these flags enabled
-@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.V2.FLAG_NAME)
+@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME)
class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
lateinit var manager: NotificationSectionsFeatureManager
private val proxyFake = DeviceConfigProxyFake()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index cc2ef53c6cdb..12cfdcfa8df5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -35,7 +35,6 @@ import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInterac
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.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.batteryController
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
index da956ec67696..8b4de2bcc26f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
@@ -22,13 +22,12 @@ package com.android.systemui.statusbar.notification.collection
* The [modifier] function will be passed an instance of a NotificationEntryBuilder. Any
* modifications made to the builder will be applied to the [entry].
*/
-inline fun modifyEntry(
- entry: NotificationEntry,
+inline fun NotificationEntry.modifyEntry(
crossinline modifier: NotificationEntryBuilder.() -> Unit
) {
- val builder = NotificationEntryBuilder(entry)
+ val builder = NotificationEntryBuilder(this)
modifier(builder)
- builder.apply(entry)
+ builder.apply(this)
}
fun getAttachState(entry: ListEntry): ListAttachState {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
new file mode 100644
index 000000000000..77d97bb7cbe9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.collection.coordinator
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+import com.android.systemui.util.settings.fakeSettings
+
+var Kosmos.lockScreenMinimalismCoordinator by
+ Kosmos.Fixture {
+ LockScreenMinimalismCoordinator(
+ bgDispatcher = testDispatcher,
+ dumpManager = dumpManager,
+ headsUpInteractor = headsUpNotificationInteractor,
+ logger = lockScreenMinimalismCoordinatorLogger,
+ scope = testScope.backgroundScope,
+ secureSettings = fakeSettings,
+ seenNotificationsInteractor = seenNotificationsInteractor,
+ statusBarStateController = statusBarStateController,
+ shadeInteractor = shadeInteractor,
+ )
+ }
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/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
index 9be7dfe9a1a9..77aeb44e15eb 100644
--- 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/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -14,14 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.stack.data.repository
+package com.android.systemui.statusbar.notification.collection.coordinator
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
-fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) {
- setNotifications(*notifications.toTypedArray())
-}
-
-fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) {
- this.activeHeadsUpRows.value = notifications.toSet()
-}
+val Kosmos.lockScreenMinimalismCoordinatorLogger by
+ Kosmos.Fixture { LockScreenMinimalismCoordinatorLogger(logcatLogBuffer()) }
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 492e87bbcac4..7e8f1a9115ea 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
@@ -22,14 +22,19 @@ import com.android.systemui.statusbar.notification.data.repository.HeadsUpReposi
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() }
class FakeHeadsUpNotificationRepository : HeadsUpRepository {
override val isHeadsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false)
- override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null)
- override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> =
- MutableStateFlow(emptySet())
+
+ val orderedHeadsUpRows = MutableStateFlow(emptyList<HeadsUpRowRepository>())
+ override val topHeadsUpRow: Flow<HeadsUpRowRepository?> =
+ orderedHeadsUpRows.map { it.firstOrNull() }.distinctUntilChanged()
+ override val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>> =
+ orderedHeadsUpRows.map { it.toSet() }.distinctUntilChanged()
override fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
isHeadsUpAnimatingAway.value = animatingAway
@@ -38,4 +43,12 @@ class FakeHeadsUpNotificationRepository : HeadsUpRepository {
override fun snooze() {
// do nothing
}
+
+ fun setNotifications(notifications: List<HeadsUpRowRepository>) {
+ this.orderedHeadsUpRows.value = notifications.toList()
+ }
+
+ fun setNotifications(vararg notifications: HeadsUpRowRepository) {
+ this.orderedHeadsUpRows.value = notifications.toList()
+ }
}