diff options
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() + } } |