Merge "Split the unseen logic out of the KeyguardCoordinator" into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt
new file mode 100644
index 0000000..6ddc074
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt
@@ -0,0 +1,683 @@
+/*
+ * Copyright (C) 2022 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.os.UserHandle
+import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.data.repository.Idle
+import com.android.systemui.scene.data.repository.setTransition
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
+import com.android.systemui.util.settings.FakeSettings
+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.ArgumentMatchers.same
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class OriginalUnseenKeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() {
+
+ private val kosmos = Kosmos()
+
+ private val headsUpManager: HeadsUpManager = mock()
+ private val keyguardRepository = FakeKeyguardRepository()
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val notifPipeline: NotifPipeline = mock()
+ private val statusBarStateController: StatusBarStateController = mock()
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ @Test
+ fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() {
+ // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
+ keyguardRepository.setKeyguardShowing(false)
+ whenever(statusBarStateController.isExpanded).thenReturn(true)
+ runKeyguardCoordinatorTest {
+ val fakeEntry = NotificationEntryBuilder().build()
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // WHEN: The keyguard is now showing
+ keyguardRepository.setKeyguardShowing(true)
+ testScheduler.runCurrent()
+
+ // THEN: The notification is recognized as "seen" and is filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+ // WHEN: The keyguard goes away
+ keyguardRepository.setKeyguardShowing(false)
+ testScheduler.runCurrent()
+
+ // THEN: The notification is shown regardless
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() {
+ // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present
+ keyguardRepository.setKeyguardShowing(false)
+ whenever(statusBarStateController.isExpanded).thenReturn(false)
+ runKeyguardCoordinatorTest {
+ val fakeEntry = NotificationEntryBuilder().build()
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // WHEN: The device transitions to AOD
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // THEN: We are no longer listening for shade expansions
+ verify(statusBarStateController, never()).addCallback(any())
+ }
+ }
+
+ @Test
+ fun unseenFilter_headsUpMarkedAsSeen() {
+ // GIVEN: Keyguard is not showing, shade is not expanded
+ keyguardRepository.setKeyguardShowing(false)
+ whenever(statusBarStateController.isExpanded).thenReturn(false)
+ runKeyguardCoordinatorTest {
+ kosmos.setTransition(
+ sceneTransition = Idle(Scenes.Gone),
+ stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+ )
+
+ // WHEN: A notification is posted
+ val fakeEntry = NotificationEntryBuilder().build()
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // WHEN: That notification is heads up
+ onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true)
+ testScheduler.runCurrent()
+
+ // WHEN: The keyguard is now showing
+ keyguardRepository.setKeyguardShowing(true)
+ kosmos.setTransition(
+ sceneTransition = Idle(Scenes.Lockscreen),
+ stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
+ )
+
+ // THEN: The notification is recognized as "seen" and is filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+ // WHEN: The keyguard goes away
+ keyguardRepository.setKeyguardShowing(false)
+ kosmos.setTransition(
+ sceneTransition = Idle(Scenes.Gone),
+ stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE)
+ )
+
+ // THEN: The notification is shown regardless
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() {
+ // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present
+ keyguardRepository.setKeyguardShowing(false)
+ whenever(statusBarStateController.isExpanded).thenReturn(true)
+ runKeyguardCoordinatorTest {
+ val fakeEntry =
+ NotificationEntryBuilder()
+ .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build())
+ .build()
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // WHEN: The keyguard is now showing
+ keyguardRepository.setKeyguardShowing(true)
+ testScheduler.runCurrent()
+
+ // THEN: The notification is recognized as "ongoing" and is not filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() {
+ // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present
+ keyguardRepository.setKeyguardShowing(false)
+ whenever(statusBarStateController.isExpanded).thenReturn(true)
+ runKeyguardCoordinatorTest {
+ val fakeEntry =
+ NotificationEntryBuilder().build().apply {
+ row =
+ mock<ExpandableNotificationRow>().apply {
+ whenever(isMediaRow).thenReturn(true)
+ }
+ }
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // WHEN: The keyguard is now showing
+ keyguardRepository.setKeyguardShowing(true)
+ testScheduler.runCurrent()
+
+ // THEN: The notification is recognized as "media" and is not filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenFilterUpdatesSeenProviderWhenSuppressing() {
+ // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
+ keyguardRepository.setKeyguardShowing(false)
+ whenever(statusBarStateController.isExpanded).thenReturn(true)
+ runKeyguardCoordinatorTest {
+ val fakeEntry = NotificationEntryBuilder().build()
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // WHEN: The keyguard is now showing
+ keyguardRepository.setKeyguardShowing(true)
+ testScheduler.runCurrent()
+
+ // THEN: The notification is recognized as "seen" and is filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+ // WHEN: The filter is cleaned up
+ unseenFilter.onCleanup()
+
+ // THEN: The SeenNotificationProvider has been updated to reflect the suppression
+ assertThat(seenNotificationsInteractor.hasFilteredOutSeenNotifications.value).isTrue()
+ }
+ }
+
+ @Test
+ fun unseenFilterInvalidatesWhenSettingChanges() {
+ // GIVEN: Keyguard is not showing, and shade is expanded
+ keyguardRepository.setKeyguardShowing(false)
+ whenever(statusBarStateController.isExpanded).thenReturn(true)
+ runKeyguardCoordinatorTest {
+ // GIVEN: A notification is present
+ val fakeEntry = NotificationEntryBuilder().build()
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // GIVEN: The setting for filtering unseen notifications is disabled
+ showOnlyUnseenNotifsOnKeyguardSetting = false
+
+ // GIVEN: The pipeline has registered the unseen filter for invalidation
+ val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock()
+ unseenFilter.setInvalidationListener(invalidationListener)
+
+ // WHEN: The keyguard is now showing
+ keyguardRepository.setKeyguardShowing(true)
+ testScheduler.runCurrent()
+
+ // THEN: The notification is not filtered out
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+
+ // WHEN: The secure setting is changed
+ showOnlyUnseenNotifsOnKeyguardSetting = true
+
+ // THEN: The pipeline is invalidated
+ verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), any())
+
+ // THEN: The notification is recognized as "seen" and is filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+ }
+ }
+
+ @Test
+ fun unseenFilterAllowsNewNotif() {
+ // GIVEN: Keyguard is showing, no notifications present
+ keyguardRepository.setKeyguardShowing(true)
+ runKeyguardCoordinatorTest {
+ // WHEN: A new notification is posted
+ val fakeEntry = NotificationEntryBuilder().build()
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // THEN: The notification is recognized as "unseen" and is not filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenFilterSeenGroupSummaryWithUnseenChild() {
+ // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
+ keyguardRepository.setKeyguardShowing(false)
+ whenever(statusBarStateController.isExpanded).thenReturn(true)
+ runKeyguardCoordinatorTest {
+ // WHEN: A new notification is posted
+ val fakeSummary = NotificationEntryBuilder().build()
+ val fakeChild =
+ NotificationEntryBuilder()
+ .setGroup(context, "group")
+ .setGroupSummary(context, false)
+ .build()
+ GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build()
+
+ collectionListener.onEntryAdded(fakeSummary)
+ collectionListener.onEntryAdded(fakeChild)
+
+ // WHEN: Keyguard is now showing, both notifications are marked as seen
+ keyguardRepository.setKeyguardShowing(true)
+ testScheduler.runCurrent()
+
+ // WHEN: The child notification is now unseen
+ collectionListener.onEntryUpdated(fakeChild)
+
+ // THEN: The summary is not filtered out, because the child is unseen
+ assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() {
+ // GIVEN: Keyguard is showing, not dozing, unseen notification is present
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setIsDozing(false)
+ runKeyguardCoordinatorTest {
+ val fakeEntry = NotificationEntryBuilder().build()
+ collectionListener.onEntryAdded(fakeEntry)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // WHEN: five seconds have passed
+ testScheduler.advanceTimeBy(5.seconds)
+ testScheduler.runCurrent()
+
+ // WHEN: Keyguard is no longer showing
+ keyguardRepository.setKeyguardShowing(false)
+ kosmos.setTransition(
+ sceneTransition = Idle(Scenes.Gone),
+ stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+ )
+
+ // WHEN: Keyguard is shown again
+ keyguardRepository.setKeyguardShowing(true)
+ kosmos.setTransition(
+ sceneTransition = Idle(Scenes.Lockscreen),
+ stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
+ )
+
+ // THEN: The notification is now recognized as "seen" and is filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+ }
+ }
+
+ @Test
+ fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() {
+ // GIVEN: Keyguard is showing, unseen notification is present
+ keyguardRepository.setKeyguardShowing(true)
+ runKeyguardCoordinatorTest {
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
+ )
+ val fakeEntry = NotificationEntryBuilder().build()
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // WHEN: Keyguard is no longer showing
+ keyguardRepository.setKeyguardShowing(false)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
+ )
+
+ // WHEN: Keyguard is shown again
+ keyguardRepository.setKeyguardShowing(true)
+ testScheduler.runCurrent()
+
+ // THEN: The notification is not recognized as "seen" and is not filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() {
+ // GIVEN: Keyguard is showing, not dozing, unseen notification is present
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setIsDozing(false)
+ runKeyguardCoordinatorTest {
+ kosmos.setTransition(
+ sceneTransition = Idle(Scenes.Lockscreen),
+ stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
+ )
+ val firstEntry = NotificationEntryBuilder().setId(1).build()
+ collectionListener.onEntryAdded(firstEntry)
+ testScheduler.runCurrent()
+
+ // WHEN: one second has passed
+ testScheduler.advanceTimeBy(1.seconds)
+ testScheduler.runCurrent()
+
+ // WHEN: another unseen notification is posted
+ val secondEntry = NotificationEntryBuilder().setId(2).build()
+ collectionListener.onEntryAdded(secondEntry)
+ testScheduler.runCurrent()
+
+ // WHEN: four more seconds have passed
+ testScheduler.advanceTimeBy(4.seconds)
+ testScheduler.runCurrent()
+
+ // WHEN: the keyguard is no longer showing
+ keyguardRepository.setKeyguardShowing(false)
+ kosmos.setTransition(
+ sceneTransition = Idle(Scenes.Gone),
+ stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+ )
+
+ // WHEN: Keyguard is shown again
+ keyguardRepository.setKeyguardShowing(true)
+ kosmos.setTransition(
+ sceneTransition = Idle(Scenes.Lockscreen),
+ stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
+ )
+
+ // THEN: The first notification is considered seen and is filtered out.
+ assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue()
+
+ // THEN: The second notification is still considered unseen and is not filtered out
+ assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() {
+ // GIVEN: Keyguard is showing, not dozing
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setIsDozing(false)
+ runKeyguardCoordinatorTest {
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // WHEN: a new notification is posted
+ val entry = NotificationEntryBuilder().setId(1).build()
+ collectionListener.onEntryAdded(entry)
+ testScheduler.runCurrent()
+
+ // WHEN: five more seconds have passed
+ testScheduler.advanceTimeBy(5.seconds)
+ testScheduler.runCurrent()
+
+ // WHEN: the notification is removed
+ collectionListener.onEntryRemoved(entry, 0)
+ testScheduler.runCurrent()
+
+ // WHEN: the notification is re-posted
+ collectionListener.onEntryAdded(entry)
+ testScheduler.runCurrent()
+
+ // WHEN: one more second has passed
+ testScheduler.advanceTimeBy(1.seconds)
+ testScheduler.runCurrent()
+
+ // WHEN: the keyguard is no longer showing
+ keyguardRepository.setKeyguardShowing(false)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // WHEN: Keyguard is shown again
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // THEN: The notification is considered unseen and is not filtered out.
+ assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() {
+ // GIVEN: Keyguard is showing, not dozing
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setIsDozing(false)
+ runKeyguardCoordinatorTest {
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // WHEN: a new notification is posted
+ val entry = NotificationEntryBuilder().setId(1).build()
+ collectionListener.onEntryAdded(entry)
+ testScheduler.runCurrent()
+
+ // WHEN: one second has passed
+ testScheduler.advanceTimeBy(1.seconds)
+ testScheduler.runCurrent()
+
+ // WHEN: the notification is removed
+ collectionListener.onEntryRemoved(entry, 0)
+ testScheduler.runCurrent()
+
+ // WHEN: the notification is re-posted
+ collectionListener.onEntryAdded(entry)
+ testScheduler.runCurrent()
+
+ // WHEN: one more second has passed
+ testScheduler.advanceTimeBy(1.seconds)
+ testScheduler.runCurrent()
+
+ // WHEN: the keyguard is no longer showing
+ keyguardRepository.setKeyguardShowing(false)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // WHEN: Keyguard is shown again
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // THEN: The notification is considered unseen and is not filtered out.
+ assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
+ }
+ }
+
+ @Test
+ fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() {
+ // GIVEN: Keyguard is showing, not dozing
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setIsDozing(false)
+ runKeyguardCoordinatorTest {
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // WHEN: a new notification is posted
+ val entry = NotificationEntryBuilder().setId(1).build()
+ collectionListener.onEntryAdded(entry)
+ testScheduler.runCurrent()
+
+ // WHEN: one second has passed
+ testScheduler.advanceTimeBy(1.seconds)
+ testScheduler.runCurrent()
+
+ // WHEN: the notification is updated
+ collectionListener.onEntryUpdated(entry)
+ testScheduler.runCurrent()
+
+ // WHEN: four more seconds have passed
+ testScheduler.advanceTimeBy(4.seconds)
+ testScheduler.runCurrent()
+
+ // WHEN: the keyguard is no longer showing
+ keyguardRepository.setKeyguardShowing(false)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // WHEN: Keyguard is shown again
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
+ )
+ testScheduler.runCurrent()
+
+ // THEN: The notification is considered unseen and is not filtered out.
+ assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
+ }
+ }
+
+ private fun runKeyguardCoordinatorTest(
+ testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
+ ) {
+ val testDispatcher = UnconfinedTestDispatcher()
+ val testScope = TestScope(testDispatcher)
+ val fakeSettings =
+ FakeSettings().apply {
+ putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+ }
+ val seenNotificationsInteractor =
+ SeenNotificationsInteractor(ActiveNotificationListRepository())
+ val keyguardCoordinator =
+ OriginalUnseenKeyguardCoordinator(
+ testDispatcher,
+ mock<DumpManager>(),
+ headsUpManager,
+ keyguardRepository,
+ kosmos.keyguardTransitionInteractor,
+ KeyguardCoordinatorLogger(logcatLogBuffer()),
+ testScope.backgroundScope,
+ fakeSettings,
+ seenNotificationsInteractor,
+ statusBarStateController,
+ )
+ keyguardCoordinator.attach(notifPipeline)
+ testScope.runTest {
+ KeyguardCoordinatorTestScope(
+ keyguardCoordinator,
+ testScope,
+ seenNotificationsInteractor,
+ fakeSettings,
+ )
+ .testBlock()
+ }
+ }
+
+ private inner class KeyguardCoordinatorTestScope(
+ private val keyguardCoordinator: OriginalUnseenKeyguardCoordinator,
+ private val scope: TestScope,
+ val seenNotificationsInteractor: SeenNotificationsInteractor,
+ private val fakeSettings: FakeSettings,
+ ) : CoroutineScope by scope {
+ val testScheduler: TestCoroutineScheduler
+ get() = scope.testScheduler
+
+ val unseenFilter: NotifFilter
+ get() = keyguardCoordinator.unseenNotifFilter
+
+ val collectionListener: NotifCollectionListener =
+ argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue
+
+ val onHeadsUpChangedListener: OnHeadsUpChangedListener
+ get() = argumentCaptor { verify(headsUpManager).addListener(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 {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 55c6790..b1b2a65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -16,62 +16,15 @@
package com.android.systemui.statusbar.notification.collection.coordinator
-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.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-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.collection.provider.SectionHeaderVisibilityProvider
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
-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
-import com.android.systemui.util.indentIfPossible
-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.Job
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-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.onEach
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.yield
/**
* Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
@@ -82,24 +35,10 @@
class KeyguardCoordinator
@Inject
constructor(
- @Background private val bgDispatcher: CoroutineDispatcher,
- private val dumpManager: DumpManager,
- private val headsUpManager: HeadsUpManager,
private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
- private val keyguardRepository: KeyguardRepository,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
- private val logger: KeyguardCoordinatorLogger,
- @Application private val scope: CoroutineScope,
private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
- private val secureSettings: SecureSettings,
- private val seenNotificationsInteractor: SeenNotificationsInteractor,
private val statusBarStateController: StatusBarStateController,
-) : Coordinator, Dumpable {
-
- private val unseenNotifications = mutableSetOf<NotificationEntry>()
- private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
- private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
- private var unseenFilterEnabled = false
+) : Coordinator {
override fun attach(pipeline: NotifPipeline) {
setupInvalidateNotifListCallbacks()
@@ -107,385 +46,14 @@
pipeline.addFinalizeFilter(notifFilter)
keyguardNotificationVisibilityProvider.addOnStateChangedListener(::invalidateListFromFilter)
updateSectionHeadersVisibility()
- attachUnseenFilter(pipeline)
}
- private fun attachUnseenFilter(pipeline: NotifPipeline) {
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- pipeline.addPromoter(unseenNotifPromoter)
- pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
- }
- pipeline.addFinalizeFilter(unseenNotifFilter)
- pipeline.addCollectionListener(collectionListener)
- scope.launch { trackUnseenFilterSettingChanges() }
- dumpManager.registerDumpable(this)
- }
-
- private suspend fun trackSeenNotifications() {
- // Whether or not keyguard is visible (or occluded).
- val isKeyguardPresent: Flow<Boolean> =
- keyguardTransitionInteractor
- .transitionValue(
- scene = Scenes.Gone,
- stateWithoutSceneContainer = KeyguardState.GONE,
- )
- .map { it == 0f }
- .distinctUntilChanged()
- .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) }
-
- // Separately track seen notifications while the device is locked, applying once the device
- // is unlocked.
- val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>()
-
- // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes.
- isKeyguardPresent.collectLatest { isKeyguardPresent: Boolean ->
- if (isKeyguardPresent) {
- // Keyguard is not gone, notifications need to be visible for a certain threshold
- // before being marked as seen
- trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked)
- } else {
- // Mark all seen-while-locked notifications as seen for real.
- if (notificationsSeenWhileLocked.isNotEmpty()) {
- unseenNotifications.removeAll(notificationsSeenWhileLocked)
- logger.logAllMarkedSeenOnUnlock(
- seenCount = notificationsSeenWhileLocked.size,
- remainingUnseenCount = unseenNotifications.size
- )
- notificationsSeenWhileLocked.clear()
- }
- unseenNotifFilter.invalidateList("keyguard no longer showing")
- // Keyguard is gone, notifications can be immediately marked as seen when they
- // become visible.
- trackSeenNotificationsWhileUnlocked()
- }
- }
- }
-
- /**
- * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
- * been "seen" while the device is on the keyguard.
- */
- private suspend fun trackSeenNotificationsWhileLocked(
- notificationsSeenWhileLocked: MutableSet<NotificationEntry>,
- ) = coroutineScope {
- // Remove removed notifications from the set
- launch {
- unseenEntryRemoved.collect { entry ->
- if (notificationsSeenWhileLocked.remove(entry)) {
- logger.logRemoveSeenOnLockscreen(entry)
- }
- }
- }
- // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and
- // is restarted when doze ends.
- keyguardRepository.isDozing.collectLatest { isDozing ->
- if (!isDozing) {
- trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked)
- }
- }
- }
-
- /**
- * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
- * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen
- * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration.
- */
- private suspend fun trackSeenNotificationsWhileLockedAndNotDozing(
- notificationsSeenWhileLocked: MutableSet<NotificationEntry>
- ) = coroutineScope {
- // All child tracking jobs will be cancelled automatically when this is cancelled.
- val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>()
-
- /**
- * Wait for the user to spend enough time on the lock screen before removing notification
- * from unseen set upon unlock.
- */
- suspend fun trackSeenDurationThreshold(entry: NotificationEntry) {
- if (notificationsSeenWhileLocked.remove(entry)) {
- logger.logResetSeenOnLockscreen(entry)
- }
- delay(SEEN_TIMEOUT)
- notificationsSeenWhileLocked.add(entry)
- trackingJobsByEntry.remove(entry)
- logger.logSeenOnLockscreen(entry)
- }
-
- /** Stop any unseen tracking when a notification is removed. */
- suspend fun stopTrackingRemovedNotifs(): Nothing =
- unseenEntryRemoved.collect { entry ->
- trackingJobsByEntry.remove(entry)?.let {
- it.cancel()
- logger.logStopTrackingLockscreenSeenDuration(entry)
- }
- }
-
- /** Start tracking new notifications when they are posted. */
- suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope {
- unseenEntryAdded.collect { entry ->
- logger.logTrackingLockscreenSeenDuration(entry)
- // If this is an update, reset the tracking.
- trackingJobsByEntry[entry]?.let {
- it.cancel()
- logger.logResetSeenOnLockscreen(entry)
- }
- trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
- }
- }
-
- // Start tracking for all notifications that are currently unseen.
- logger.logTrackingLockscreenSeenDuration(unseenNotifications)
- unseenNotifications.forEach { entry ->
- trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
- }
-
- launch { trackNewUnseenNotifs() }
- launch { stopTrackingRemovedNotifs() }
- }
-
- // Track "seen" notifications, marking them as such when either shade is expanded or the
- // notification becomes heads up.
- private suspend fun trackSeenNotificationsWhileUnlocked() {
- coroutineScope {
- launch { clearUnseenNotificationsWhenShadeIsExpanded() }
- launch { markHeadsUpNotificationsAsSeen() }
- }
- }
-
- private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
- statusBarStateController.expansionChanges.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
- yield()
- if (isExpanded) {
- logger.logShadeExpanded()
- unseenNotifications.clear()
- }
- }
- }
-
- private suspend fun markHeadsUpNotificationsAsSeen() {
- headsUpManager.allEntries
- .filter { it.isRowPinned }
- .forEach { unseenNotifications.remove(it) }
- headsUpManager.headsUpEvents.collect { (entry, isHun) ->
- if (isHun) {
- logger.logUnseenHun(entry.key)
- unseenNotifications.remove(entry)
- }
- }
- }
-
- private fun unseenFeatureEnabled(): Flow<Boolean> {
- if (
- NotificationMinimalismPrototype.V1.isEnabled ||
- NotificationMinimalismPrototype.V2.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 { setting ->
- // update local field and invalidate if necessary
- if (setting != unseenFilterEnabled) {
- unseenFilterEnabled = setting
- unseenNotifFilter.invalidateList("unseen setting changed")
- }
- // if the setting is enabled, then start tracking and filtering unseen notifications
- if (setting) {
- trackSeenNotifications()
- }
- }
- }
-
- private val collectionListener =
- object : NotifCollectionListener {
- override fun onEntryAdded(entry: NotificationEntry) {
- if (
- keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
- ) {
- logger.logUnseenAdded(entry.key)
- unseenNotifications.add(entry)
- unseenEntryAdded.tryEmit(entry)
- }
- }
-
- override fun onEntryUpdated(entry: NotificationEntry) {
- if (
- keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
- ) {
- logger.logUnseenUpdated(entry.key)
- unseenNotifications.add(entry)
- unseenEntryAdded.tryEmit(entry)
- }
- }
-
- override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
- if (unseenNotifications.remove(entry)) {
- logger.logUnseenRemoved(entry.key)
- unseenEntryRemoved.tryEmit(entry)
- }
- }
- }
-
- 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
- internal 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
- internal val unseenNotifFilter =
- object : NotifFilter("$TAG-unseen") {
-
- 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
- // Don't apply the filter if the notification is unseen
- unseenNotifications.contains(entry) -> false
- // Don't apply the filter to (non-promoted) group summaries
- // - summary will be pruned if necessary, depending on if children are filtered
- entry.parent?.summary == entry -> false
- // Check that the entry satisfies certain characteristics that would bypass the
- // filter
- shouldIgnoreUnseenCheck(entry) -> false
- else -> true
- }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered }
-
- override fun onCleanup() {
- logger.logProviderHasFilteredOutSeenNotifs(hasFilteredAnyNotifs)
- seenNotificationsInteractor.setHasFilteredOutSeenNotifications(hasFilteredAnyNotifs)
- hasFilteredAnyNotifs = false
- }
- }
-
private val notifFilter: NotifFilter =
object : NotifFilter(TAG) {
override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
}
- private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean =
- when {
- entry.isMediaNotification -> true
- entry.sbn.isOngoing -> true
- else -> false
- }
-
// TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on
// these same updates
private fun setupInvalidateNotifListCallbacks() {}
@@ -502,22 +70,7 @@
sectionHeaderVisibilityProvider.sectionHeadersVisible = showSections
}
- override fun dump(pw: PrintWriter, args: Array<out String>) =
- with(pw.asIndenting()) {
- println(
- "notificationListInteractor.hasFilteredOutSeenNotifications.value=" +
- seenNotificationsInteractor.hasFilteredOutSeenNotifications.value
- )
- println("unseen notifications:")
- indentIfPossible {
- for (notification in unseenNotifications) {
- println(notification.key)
- }
- }
- }
-
companion object {
private const val TAG = "KeyguardCoordinator"
- private val SEEN_TIMEOUT = 5.seconds
}
}
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 e038982..99327d1 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
@@ -17,7 +17,11 @@
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED
-import com.android.systemui.statusbar.notification.collection.*
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag
+import com.android.systemui.statusbar.notification.collection.PipelineDumpable
+import com.android.systemui.statusbar.notification.collection.PipelineDumper
+import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
@@ -42,6 +46,7 @@
hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator,
hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
keyguardCoordinator: KeyguardCoordinator,
+ unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator,
rankingCoordinator: RankingCoordinator,
colorizedFgsCoordinator: ColorizedFgsCoordinator,
deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
@@ -82,6 +87,7 @@
mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
mCoordinators.add(hideNotifsForOtherUsersCoordinator)
mCoordinators.add(keyguardCoordinator)
+ mCoordinators.add(unseenKeyguardCoordinator)
mCoordinators.add(rankingCoordinator)
mCoordinators.add(colorizedFgsCoordinator)
mCoordinators.add(deviceProvisionedCoordinator)
@@ -115,11 +121,11 @@
// Manually add Ordered Sections
if (NotificationMinimalismPrototype.V2.isEnabled) {
- mOrderedSections.add(keyguardCoordinator.topOngoingSectioner) // Top Ongoing
+ mOrderedSections.add(unseenKeyguardCoordinator.topOngoingSectioner) // Top Ongoing
}
mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
if (NotificationMinimalismPrototype.V2.isEnabled) {
- mOrderedSections.add(keyguardCoordinator.topUnseenSectioner) // Top Unseen
+ mOrderedSections.add(unseenKeyguardCoordinator.topUnseenSectioner) // Top Unseen
}
mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
if (PriorityPeopleSection.isEnabled) {
@@ -131,10 +137,10 @@
}
mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting
if (NotificationClassificationFlag.isEnabled) {
- mOrderedSections.add(bundleCoordinator.newsSectioner);
- mOrderedSections.add(bundleCoordinator.socialSectioner);
- mOrderedSections.add(bundleCoordinator.recsSectioner);
- mOrderedSections.add(bundleCoordinator.promoSectioner);
+ mOrderedSections.add(bundleCoordinator.newsSectioner)
+ mOrderedSections.add(bundleCoordinator.socialSectioner)
+ mOrderedSections.add(bundleCoordinator.recsSectioner)
+ mOrderedSections.add(bundleCoordinator.promoSectioner)
}
mOrderedSections.add(rankingCoordinator.silentSectioner) // Silent
mOrderedSections.add(rankingCoordinator.minimizedSectioner) // Minimized
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
new file mode 100644
index 0000000..5dd1663
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -0,0 +1,490 @@
+/*
+ * Copyright (C) 2022 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.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+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
+import com.android.systemui.util.indentIfPossible
+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.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+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.onEach
+import kotlinx.coroutines.flow.onStart
+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.
+ */
+@CoordinatorScope
+@SuppressLint("SharedFlowCreation")
+class OriginalUnseenKeyguardCoordinator
+@Inject
+constructor(
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val dumpManager: DumpManager,
+ private val headsUpManager: HeadsUpManager,
+ private val keyguardRepository: KeyguardRepository,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val logger: KeyguardCoordinatorLogger,
+ @Application private val scope: CoroutineScope,
+ private val secureSettings: SecureSettings,
+ private val seenNotificationsInteractor: SeenNotificationsInteractor,
+ private val statusBarStateController: StatusBarStateController,
+) : Coordinator, Dumpable {
+
+ private val unseenNotifications = mutableSetOf<NotificationEntry>()
+ private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
+ private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
+ private var unseenFilterEnabled = false
+
+ override fun attach(pipeline: NotifPipeline) {
+ if (NotificationMinimalismPrototype.V2.isEnabled) {
+ pipeline.addPromoter(unseenNotifPromoter)
+ pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
+ }
+ pipeline.addFinalizeFilter(unseenNotifFilter)
+ pipeline.addCollectionListener(collectionListener)
+ scope.launch { trackUnseenFilterSettingChanges() }
+ dumpManager.registerDumpable(this)
+ }
+
+ private suspend fun trackSeenNotifications() {
+ // Whether or not keyguard is visible (or occluded).
+ val isKeyguardPresentFlow: Flow<Boolean> =
+ keyguardTransitionInteractor
+ .transitionValue(
+ scene = Scenes.Gone,
+ stateWithoutSceneContainer = KeyguardState.GONE,
+ )
+ .map { it == 0f }
+ .distinctUntilChanged()
+ .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) }
+
+ // Separately track seen notifications while the device is locked, applying once the device
+ // is unlocked.
+ val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>()
+
+ // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes.
+ isKeyguardPresentFlow.collectLatest { isKeyguardPresent: Boolean ->
+ if (isKeyguardPresent) {
+ // Keyguard is not gone, notifications need to be visible for a certain threshold
+ // before being marked as seen
+ trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked)
+ } else {
+ // Mark all seen-while-locked notifications as seen for real.
+ if (notificationsSeenWhileLocked.isNotEmpty()) {
+ unseenNotifications.removeAll(notificationsSeenWhileLocked)
+ logger.logAllMarkedSeenOnUnlock(
+ seenCount = notificationsSeenWhileLocked.size,
+ remainingUnseenCount = unseenNotifications.size
+ )
+ notificationsSeenWhileLocked.clear()
+ }
+ unseenNotifFilter.invalidateList("keyguard no longer showing")
+ // Keyguard is gone, notifications can be immediately marked as seen when they
+ // become visible.
+ trackSeenNotificationsWhileUnlocked()
+ }
+ }
+ }
+
+ /**
+ * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
+ * been "seen" while the device is on the keyguard.
+ */
+ private suspend fun trackSeenNotificationsWhileLocked(
+ notificationsSeenWhileLocked: MutableSet<NotificationEntry>,
+ ) = coroutineScope {
+ // Remove removed notifications from the set
+ launch {
+ unseenEntryRemoved.collect { entry ->
+ if (notificationsSeenWhileLocked.remove(entry)) {
+ logger.logRemoveSeenOnLockscreen(entry)
+ }
+ }
+ }
+ // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and
+ // is restarted when doze ends.
+ keyguardRepository.isDozing.collectLatest { isDozing ->
+ if (!isDozing) {
+ trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked)
+ }
+ }
+ }
+
+ /**
+ * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
+ * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen
+ * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration.
+ */
+ private suspend fun trackSeenNotificationsWhileLockedAndNotDozing(
+ notificationsSeenWhileLocked: MutableSet<NotificationEntry>
+ ) = coroutineScope {
+ // All child tracking jobs will be cancelled automatically when this is cancelled.
+ val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>()
+
+ /**
+ * Wait for the user to spend enough time on the lock screen before removing notification
+ * from unseen set upon unlock.
+ */
+ suspend fun trackSeenDurationThreshold(entry: NotificationEntry) {
+ if (notificationsSeenWhileLocked.remove(entry)) {
+ logger.logResetSeenOnLockscreen(entry)
+ }
+ delay(SEEN_TIMEOUT)
+ notificationsSeenWhileLocked.add(entry)
+ trackingJobsByEntry.remove(entry)
+ logger.logSeenOnLockscreen(entry)
+ }
+
+ /** Stop any unseen tracking when a notification is removed. */
+ suspend fun stopTrackingRemovedNotifs(): Nothing =
+ unseenEntryRemoved.collect { entry ->
+ trackingJobsByEntry.remove(entry)?.let {
+ it.cancel()
+ logger.logStopTrackingLockscreenSeenDuration(entry)
+ }
+ }
+
+ /** Start tracking new notifications when they are posted. */
+ suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope {
+ unseenEntryAdded.collect { entry ->
+ logger.logTrackingLockscreenSeenDuration(entry)
+ // If this is an update, reset the tracking.
+ trackingJobsByEntry[entry]?.let {
+ it.cancel()
+ logger.logResetSeenOnLockscreen(entry)
+ }
+ trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
+ }
+ }
+
+ // Start tracking for all notifications that are currently unseen.
+ logger.logTrackingLockscreenSeenDuration(unseenNotifications)
+ unseenNotifications.forEach { entry ->
+ trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
+ }
+
+ launch { trackNewUnseenNotifs() }
+ launch { stopTrackingRemovedNotifs() }
+ }
+
+ // Track "seen" notifications, marking them as such when either shade is expanded or the
+ // notification becomes heads up.
+ private suspend fun trackSeenNotificationsWhileUnlocked() {
+ coroutineScope {
+ launch { clearUnseenNotificationsWhenShadeIsExpanded() }
+ launch { markHeadsUpNotificationsAsSeen() }
+ }
+ }
+
+ private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
+ statusBarStateController.expansionChanges.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
+ yield()
+ if (isExpanded) {
+ logger.logShadeExpanded()
+ unseenNotifications.clear()
+ }
+ }
+ }
+
+ private suspend fun markHeadsUpNotificationsAsSeen() {
+ headsUpManager.allEntries
+ .filter { it.isRowPinned }
+ .forEach { unseenNotifications.remove(it) }
+ headsUpManager.headsUpEvents.collect { (entry, isHun) ->
+ if (isHun) {
+ logger.logUnseenHun(entry.key)
+ unseenNotifications.remove(entry)
+ }
+ }
+ }
+
+ private fun unseenFeatureEnabled(): Flow<Boolean> {
+ if (
+ NotificationMinimalismPrototype.V1.isEnabled ||
+ NotificationMinimalismPrototype.V2.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 { setting ->
+ // update local field and invalidate if necessary
+ if (setting != unseenFilterEnabled) {
+ unseenFilterEnabled = setting
+ unseenNotifFilter.invalidateList("unseen setting changed")
+ }
+ // if the setting is enabled, then start tracking and filtering unseen notifications
+ if (setting) {
+ trackSeenNotifications()
+ }
+ }
+ }
+
+ private val collectionListener =
+ object : NotifCollectionListener {
+ override fun onEntryAdded(entry: NotificationEntry) {
+ if (
+ keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
+ ) {
+ logger.logUnseenAdded(entry.key)
+ unseenNotifications.add(entry)
+ unseenEntryAdded.tryEmit(entry)
+ }
+ }
+
+ override fun onEntryUpdated(entry: NotificationEntry) {
+ if (
+ keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
+ ) {
+ logger.logUnseenUpdated(entry.key)
+ unseenNotifications.add(entry)
+ unseenEntryAdded.tryEmit(entry)
+ }
+ }
+
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ if (unseenNotifications.remove(entry)) {
+ logger.logUnseenRemoved(entry.key)
+ unseenEntryRemoved.tryEmit(entry)
+ }
+ }
+ }
+
+ 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") {
+
+ 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
+ // Don't apply the filter if the notification is unseen
+ unseenNotifications.contains(entry) -> false
+ // Don't apply the filter to (non-promoted) group summaries
+ // - summary will be pruned if necessary, depending on if children are filtered
+ entry.parent?.summary == entry -> false
+ // Check that the entry satisfies certain characteristics that would bypass the
+ // filter
+ shouldIgnoreUnseenCheck(entry) -> false
+ else -> true
+ }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered }
+
+ override fun onCleanup() {
+ logger.logProviderHasFilteredOutSeenNotifs(hasFilteredAnyNotifs)
+ seenNotificationsInteractor.setHasFilteredOutSeenNotifications(hasFilteredAnyNotifs)
+ hasFilteredAnyNotifs = false
+ }
+ }
+
+ private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean =
+ when {
+ entry.isMediaNotification -> true
+ entry.sbn.isOngoing -> true
+ else -> false
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) =
+ with(pw.asIndenting()) {
+ println(
+ "notificationListInteractor.hasFilteredOutSeenNotifications.value=" +
+ seenNotificationsInteractor.hasFilteredOutSeenNotifications.value
+ )
+ println("unseen notifications:")
+ indentIfPossible {
+ for (notification in unseenNotifications) {
+ println(notification.key)
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "OriginalUnseenKeyguardCoordinator"
+ private val SEEN_TIMEOUT = 5.seconds
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
index d87b3e2..4218be2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -13,88 +13,57 @@
* 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.os.UserHandle
-import android.platform.test.flag.junit.FlagsParameterization
-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.dump.DumpManager
-import com.android.systemui.flags.andSceneContainer
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.scene.data.repository.Idle
-import com.android.systemui.scene.data.repository.setTransition
-import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
-import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
-import com.android.systemui.statusbar.policy.HeadsUpManager
-import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.withArgCaptor
-import com.android.systemui.util.settings.FakeSettings
-import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
-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.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.same
-import org.mockito.Mockito.anyString
import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
@SmallTest
-@RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() {
+@RunWith(AndroidJUnit4::class)
+class KeyguardCoordinatorTest : SysuiTestCase() {
- private val kosmos = Kosmos()
-
- private val headsUpManager: HeadsUpManager = mock()
private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
- private val keyguardRepository = FakeKeyguardRepository()
- private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val notifPipeline: NotifPipeline = mock()
private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
private val statusBarStateController: StatusBarStateController = mock()
- init {
- mSetFlagsRule.setFlagsParameterization(flags)
+ private lateinit var onStateChangeListener: Consumer<String>
+
+ @Before
+ fun setup() {
+ val keyguardCoordinator =
+ KeyguardCoordinator(
+ keyguardNotifVisibilityProvider,
+ sectionHeaderVisibilityProvider,
+ statusBarStateController,
+ )
+ keyguardCoordinator.attach(notifPipeline)
+ onStateChangeListener =
+ argumentCaptor {
+ verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
+ }
+ .lastValue
}
@Test
- fun testSetSectionHeadersVisibleInShade() = runKeyguardCoordinatorTest {
+ fun testSetSectionHeadersVisibleInShade() {
clearInvocations(sectionHeaderVisibilityProvider)
whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
onStateChangeListener.accept("state change")
@@ -102,617 +71,10 @@
}
@Test
- fun testSetSectionHeadersNotVisibleOnKeyguard() = runKeyguardCoordinatorTest {
+ fun testSetSectionHeadersNotVisibleOnKeyguard() {
clearInvocations(sectionHeaderVisibilityProvider)
whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
onStateChangeListener.accept("state change")
verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false)
}
-
- @Test
- fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() {
- // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
- keyguardRepository.setKeyguardShowing(false)
- whenever(statusBarStateController.isExpanded).thenReturn(true)
- runKeyguardCoordinatorTest {
- val fakeEntry = NotificationEntryBuilder().build()
- collectionListener.onEntryAdded(fakeEntry)
-
- // WHEN: The keyguard is now showing
- keyguardRepository.setKeyguardShowing(true)
- testScheduler.runCurrent()
-
- // THEN: The notification is recognized as "seen" and is filtered out.
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
-
- // WHEN: The keyguard goes away
- keyguardRepository.setKeyguardShowing(false)
- testScheduler.runCurrent()
-
- // THEN: The notification is shown regardless
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() {
- // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present
- keyguardRepository.setKeyguardShowing(false)
- whenever(statusBarStateController.isExpanded).thenReturn(false)
- runKeyguardCoordinatorTest {
- val fakeEntry = NotificationEntryBuilder().build()
- collectionListener.onEntryAdded(fakeEntry)
-
- // WHEN: The device transitions to AOD
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GONE,
- to = KeyguardState.AOD,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // THEN: We are no longer listening for shade expansions
- verify(statusBarStateController, never()).addCallback(any())
- }
- }
-
- @Test
- fun unseenFilter_headsUpMarkedAsSeen() {
- // GIVEN: Keyguard is not showing, shade is not expanded
- keyguardRepository.setKeyguardShowing(false)
- whenever(statusBarStateController.isExpanded).thenReturn(false)
- runKeyguardCoordinatorTest {
- kosmos.setTransition(
- sceneTransition = Idle(Scenes.Gone),
- stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
- )
-
- // WHEN: A notification is posted
- val fakeEntry = NotificationEntryBuilder().build()
- collectionListener.onEntryAdded(fakeEntry)
-
- // WHEN: That notification is heads up
- onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true)
- testScheduler.runCurrent()
-
- // WHEN: The keyguard is now showing
- keyguardRepository.setKeyguardShowing(true)
- kosmos.setTransition(
- sceneTransition = Idle(Scenes.Lockscreen),
- stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
- )
-
- // THEN: The notification is recognized as "seen" and is filtered out.
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
-
- // WHEN: The keyguard goes away
- keyguardRepository.setKeyguardShowing(false)
- kosmos.setTransition(
- sceneTransition = Idle(Scenes.Gone),
- stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE)
- )
-
- // THEN: The notification is shown regardless
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() {
- // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present
- keyguardRepository.setKeyguardShowing(false)
- whenever(statusBarStateController.isExpanded).thenReturn(true)
- runKeyguardCoordinatorTest {
- val fakeEntry =
- NotificationEntryBuilder()
- .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build())
- .build()
- collectionListener.onEntryAdded(fakeEntry)
-
- // WHEN: The keyguard is now showing
- keyguardRepository.setKeyguardShowing(true)
- testScheduler.runCurrent()
-
- // THEN: The notification is recognized as "ongoing" and is not filtered out.
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() {
- // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present
- keyguardRepository.setKeyguardShowing(false)
- whenever(statusBarStateController.isExpanded).thenReturn(true)
- runKeyguardCoordinatorTest {
- val fakeEntry =
- NotificationEntryBuilder().build().apply {
- row =
- mock<ExpandableNotificationRow>().apply {
- whenever(isMediaRow).thenReturn(true)
- }
- }
- collectionListener.onEntryAdded(fakeEntry)
-
- // WHEN: The keyguard is now showing
- keyguardRepository.setKeyguardShowing(true)
- testScheduler.runCurrent()
-
- // THEN: The notification is recognized as "media" and is not filtered out.
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenFilterUpdatesSeenProviderWhenSuppressing() {
- // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
- keyguardRepository.setKeyguardShowing(false)
- whenever(statusBarStateController.isExpanded).thenReturn(true)
- runKeyguardCoordinatorTest {
- val fakeEntry = NotificationEntryBuilder().build()
- collectionListener.onEntryAdded(fakeEntry)
-
- // WHEN: The keyguard is now showing
- keyguardRepository.setKeyguardShowing(true)
- testScheduler.runCurrent()
-
- // THEN: The notification is recognized as "seen" and is filtered out.
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
-
- // WHEN: The filter is cleaned up
- unseenFilter.onCleanup()
-
- // THEN: The SeenNotificationProvider has been updated to reflect the suppression
- assertThat(seenNotificationsInteractor.hasFilteredOutSeenNotifications.value).isTrue()
- }
- }
-
- @Test
- fun unseenFilterInvalidatesWhenSettingChanges() {
- // GIVEN: Keyguard is not showing, and shade is expanded
- keyguardRepository.setKeyguardShowing(false)
- whenever(statusBarStateController.isExpanded).thenReturn(true)
- runKeyguardCoordinatorTest {
- // GIVEN: A notification is present
- val fakeEntry = NotificationEntryBuilder().build()
- collectionListener.onEntryAdded(fakeEntry)
-
- // GIVEN: The setting for filtering unseen notifications is disabled
- showOnlyUnseenNotifsOnKeyguardSetting = false
-
- // GIVEN: The pipeline has registered the unseen filter for invalidation
- val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock()
- unseenFilter.setInvalidationListener(invalidationListener)
-
- // WHEN: The keyguard is now showing
- keyguardRepository.setKeyguardShowing(true)
- testScheduler.runCurrent()
-
- // THEN: The notification is not filtered out
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
-
- // WHEN: The secure setting is changed
- showOnlyUnseenNotifsOnKeyguardSetting = true
-
- // THEN: The pipeline is invalidated
- verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), anyString())
-
- // THEN: The notification is recognized as "seen" and is filtered out.
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
- }
- }
-
- @Test
- fun unseenFilterAllowsNewNotif() {
- // GIVEN: Keyguard is showing, no notifications present
- keyguardRepository.setKeyguardShowing(true)
- runKeyguardCoordinatorTest {
- // WHEN: A new notification is posted
- val fakeEntry = NotificationEntryBuilder().build()
- collectionListener.onEntryAdded(fakeEntry)
-
- // THEN: The notification is recognized as "unseen" and is not filtered out.
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenFilterSeenGroupSummaryWithUnseenChild() {
- // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
- keyguardRepository.setKeyguardShowing(false)
- whenever(statusBarStateController.isExpanded).thenReturn(true)
- runKeyguardCoordinatorTest {
- // WHEN: A new notification is posted
- val fakeSummary = NotificationEntryBuilder().build()
- val fakeChild =
- NotificationEntryBuilder()
- .setGroup(context, "group")
- .setGroupSummary(context, false)
- .build()
- GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build()
-
- collectionListener.onEntryAdded(fakeSummary)
- collectionListener.onEntryAdded(fakeChild)
-
- // WHEN: Keyguard is now showing, both notifications are marked as seen
- keyguardRepository.setKeyguardShowing(true)
- testScheduler.runCurrent()
-
- // WHEN: The child notification is now unseen
- collectionListener.onEntryUpdated(fakeChild)
-
- // THEN: The summary is not filtered out, because the child is unseen
- assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() {
- // GIVEN: Keyguard is showing, not dozing, unseen notification is present
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setIsDozing(false)
- runKeyguardCoordinatorTest {
- val fakeEntry = NotificationEntryBuilder().build()
- collectionListener.onEntryAdded(fakeEntry)
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.AOD,
- to = KeyguardState.LOCKSCREEN,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // WHEN: five seconds have passed
- testScheduler.advanceTimeBy(5.seconds)
- testScheduler.runCurrent()
-
- // WHEN: Keyguard is no longer showing
- keyguardRepository.setKeyguardShowing(false)
- kosmos.setTransition(
- sceneTransition = Idle(Scenes.Gone),
- stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
- )
-
- // WHEN: Keyguard is shown again
- keyguardRepository.setKeyguardShowing(true)
- kosmos.setTransition(
- sceneTransition = Idle(Scenes.Lockscreen),
- stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
- )
-
- // THEN: The notification is now recognized as "seen" and is filtered out.
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
- }
- }
-
- @Test
- fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() {
- // GIVEN: Keyguard is showing, unseen notification is present
- keyguardRepository.setKeyguardShowing(true)
- runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GONE,
- to = KeyguardState.LOCKSCREEN,
- this.testScheduler,
- )
- val fakeEntry = NotificationEntryBuilder().build()
- collectionListener.onEntryAdded(fakeEntry)
-
- // WHEN: Keyguard is no longer showing
- keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- this.testScheduler,
- )
-
- // WHEN: Keyguard is shown again
- keyguardRepository.setKeyguardShowing(true)
- testScheduler.runCurrent()
-
- // THEN: The notification is not recognized as "seen" and is not filtered out.
- assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() {
- // GIVEN: Keyguard is showing, not dozing, unseen notification is present
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setIsDozing(false)
- runKeyguardCoordinatorTest {
- kosmos.setTransition(
- sceneTransition = Idle(Scenes.Lockscreen),
- stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
- )
- val firstEntry = NotificationEntryBuilder().setId(1).build()
- collectionListener.onEntryAdded(firstEntry)
- testScheduler.runCurrent()
-
- // WHEN: one second has passed
- testScheduler.advanceTimeBy(1.seconds)
- testScheduler.runCurrent()
-
- // WHEN: another unseen notification is posted
- val secondEntry = NotificationEntryBuilder().setId(2).build()
- collectionListener.onEntryAdded(secondEntry)
- testScheduler.runCurrent()
-
- // WHEN: four more seconds have passed
- testScheduler.advanceTimeBy(4.seconds)
- testScheduler.runCurrent()
-
- // WHEN: the keyguard is no longer showing
- keyguardRepository.setKeyguardShowing(false)
- kosmos.setTransition(
- sceneTransition = Idle(Scenes.Gone),
- stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
- )
-
- // WHEN: Keyguard is shown again
- keyguardRepository.setKeyguardShowing(true)
- kosmos.setTransition(
- sceneTransition = Idle(Scenes.Lockscreen),
- stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
- )
-
- // THEN: The first notification is considered seen and is filtered out.
- assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue()
-
- // THEN: The second notification is still considered unseen and is not filtered out
- assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() {
- // GIVEN: Keyguard is showing, not dozing
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setIsDozing(false)
- runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GONE,
- to = KeyguardState.LOCKSCREEN,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // WHEN: a new notification is posted
- val entry = NotificationEntryBuilder().setId(1).build()
- collectionListener.onEntryAdded(entry)
- testScheduler.runCurrent()
-
- // WHEN: five more seconds have passed
- testScheduler.advanceTimeBy(5.seconds)
- testScheduler.runCurrent()
-
- // WHEN: the notification is removed
- collectionListener.onEntryRemoved(entry, 0)
- testScheduler.runCurrent()
-
- // WHEN: the notification is re-posted
- collectionListener.onEntryAdded(entry)
- testScheduler.runCurrent()
-
- // WHEN: one more second has passed
- testScheduler.advanceTimeBy(1.seconds)
- testScheduler.runCurrent()
-
- // WHEN: the keyguard is no longer showing
- keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // WHEN: Keyguard is shown again
- keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GONE,
- to = KeyguardState.LOCKSCREEN,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // THEN: The notification is considered unseen and is not filtered out.
- assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() {
- // GIVEN: Keyguard is showing, not dozing
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setIsDozing(false)
- runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GONE,
- to = KeyguardState.LOCKSCREEN,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // WHEN: a new notification is posted
- val entry = NotificationEntryBuilder().setId(1).build()
- collectionListener.onEntryAdded(entry)
- testScheduler.runCurrent()
-
- // WHEN: one second has passed
- testScheduler.advanceTimeBy(1.seconds)
- testScheduler.runCurrent()
-
- // WHEN: the notification is removed
- collectionListener.onEntryRemoved(entry, 0)
- testScheduler.runCurrent()
-
- // WHEN: the notification is re-posted
- collectionListener.onEntryAdded(entry)
- testScheduler.runCurrent()
-
- // WHEN: one more second has passed
- testScheduler.advanceTimeBy(1.seconds)
- testScheduler.runCurrent()
-
- // WHEN: the keyguard is no longer showing
- keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // WHEN: Keyguard is shown again
- keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GONE,
- to = KeyguardState.LOCKSCREEN,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // THEN: The notification is considered unseen and is not filtered out.
- assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
- }
- }
-
- @Test
- fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() {
- // GIVEN: Keyguard is showing, not dozing
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setIsDozing(false)
- runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GONE,
- to = KeyguardState.LOCKSCREEN,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // WHEN: a new notification is posted
- val entry = NotificationEntryBuilder().setId(1).build()
- collectionListener.onEntryAdded(entry)
- testScheduler.runCurrent()
-
- // WHEN: one second has passed
- testScheduler.advanceTimeBy(1.seconds)
- testScheduler.runCurrent()
-
- // WHEN: the notification is updated
- collectionListener.onEntryUpdated(entry)
- testScheduler.runCurrent()
-
- // WHEN: four more seconds have passed
- testScheduler.advanceTimeBy(4.seconds)
- testScheduler.runCurrent()
-
- // WHEN: the keyguard is no longer showing
- keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // WHEN: Keyguard is shown again
- keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GONE,
- to = KeyguardState.LOCKSCREEN,
- this.testScheduler,
- )
- testScheduler.runCurrent()
-
- // THEN: The notification is considered unseen and is not filtered out.
- assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
- }
- }
-
- private fun runKeyguardCoordinatorTest(
- testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
- ) {
- val testDispatcher = UnconfinedTestDispatcher()
- val testScope = TestScope(testDispatcher)
- val fakeSettings =
- FakeSettings().apply {
- putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
- }
- val seenNotificationsInteractor =
- SeenNotificationsInteractor(ActiveNotificationListRepository())
- val keyguardCoordinator =
- KeyguardCoordinator(
- testDispatcher,
- mock<DumpManager>(),
- headsUpManager,
- keyguardNotifVisibilityProvider,
- keyguardRepository,
- kosmos.keyguardTransitionInteractor,
- KeyguardCoordinatorLogger(logcatLogBuffer()),
- testScope.backgroundScope,
- sectionHeaderVisibilityProvider,
- fakeSettings,
- seenNotificationsInteractor,
- statusBarStateController,
- )
- keyguardCoordinator.attach(notifPipeline)
- testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
- KeyguardCoordinatorTestScope(
- keyguardCoordinator,
- testScope,
- seenNotificationsInteractor,
- fakeSettings,
- )
- .testBlock()
- }
- }
-
- private inner class KeyguardCoordinatorTestScope(
- private val keyguardCoordinator: KeyguardCoordinator,
- private val scope: TestScope,
- val seenNotificationsInteractor: SeenNotificationsInteractor,
- private val fakeSettings: FakeSettings,
- ) : CoroutineScope by scope {
- val testScheduler: TestCoroutineScheduler
- get() = scope.testScheduler
-
- val onStateChangeListener: Consumer<String> = withArgCaptor {
- verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
- }
-
- val unseenFilter: NotifFilter
- get() = keyguardCoordinator.unseenNotifFilter
-
- val collectionListener: NotifCollectionListener = withArgCaptor {
- verify(notifPipeline).addCollectionListener(capture())
- }
-
- val onHeadsUpChangedListener: OnHeadsUpChangedListener
- get() = withArgCaptor { verify(headsUpManager).addListener(capture()) }
-
- val statusBarStateListener: StatusBarStateController.StateListener
- get() = withArgCaptor { verify(statusBarStateController).addCallback(capture()) }
-
- 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 {
- @JvmStatic
- @Parameters(name = "{0}")
- fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf().andSceneContainer()
- }
- }
}