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 706594cee81d6..2fa070ca20b5b 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 @@ -28,9 +28,12 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.expansionChanges +import com.android.systemui.statusbar.notification.NotifPipelineFlags 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 @@ -47,29 +50,30 @@ 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 import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -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.emitAll +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.transformLatest 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. + * headers on the lockscreen. */ @CoordinatorScope class KeyguardCoordinator @@ -82,6 +86,7 @@ constructor( private val keyguardRepository: KeyguardRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val logger: KeyguardCoordinatorLogger, + private val notifPipelineFlags: NotifPipelineFlags, @Application private val scope: CoroutineScope, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, private val secureSettings: SecureSettings, @@ -90,8 +95,6 @@ constructor( ) : Coordinator, Dumpable { private val unseenNotifications = mutableSetOf() - private val unseenEntryAdded = MutableSharedFlow(extraBufferCapacity = 1) - private val unseenEntryRemoved = MutableSharedFlow(extraBufferCapacity = 1) private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { @@ -106,131 +109,79 @@ constructor( private fun attachUnseenFilter(pipeline: NotifPipeline) { pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) - scope.launch { trackSeenNotifications() } + scope.launch { trackUnseenNotificationsWhileUnlocked() } scope.launch { invalidateWhenUnseenSettingChanges() } dumpManager.registerDumpable(this) } - private suspend fun trackSeenNotifications() { - // Whether or not keyguard is visible (or occluded). - val isKeyguardPresent: Flow = - keyguardTransitionRepository.transitions - .map { step -> step.to != KeyguardState.GONE } + private suspend fun trackUnseenNotificationsWhileUnlocked() { + // Whether or not we're actively tracking unseen notifications to mark them as seen when + // appropriate. + val isTrackingUnseen: Flow = + keyguardRepository.isKeyguardShowing + // transformLatest so that we can cancel listening to keyguard transitions once + // isKeyguardShowing changes (after a successful transition to the keyguard). + .transformLatest { isShowing -> + if (isShowing) { + // If the keyguard is showing, we're not tracking unseen. + emit(false) + } else { + // If the keyguard stops showing, then start tracking unseen notifications. + emit(true) + // If the screen is turning off, stop tracking, but if that transition is + // cancelled, then start again. + emitAll( + keyguardTransitionRepository.transitions.map { step -> + !step.isScreenTurningOff + } + ) + } + } + // Prevent double emit of `false` caused by transition to AOD, followed by keyguard + // showing .distinctUntilChanged() .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } - // Separately track seen notifications while the device is locked, applying once the device - // is unlocked. - val notificationsSeenWhileLocked = mutableSetOf() - - // 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) + // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is + // showing again + var clearUnseenOnBeginTracking = false + isTrackingUnseen.collectLatest { trackingUnseen -> + if (!trackingUnseen) { + // Wait for the user to spend enough time on the lock screen before clearing unseen + // set when unlocked + awaitTimeSpentNotDozing(SEEN_TIMEOUT) + clearUnseenOnBeginTracking = true + logger.logSeenOnLockscreen() } 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() + if (clearUnseenOnBeginTracking) { + clearUnseenOnBeginTracking = false + logger.logAllMarkedSeenOnUnlock() + unseenNotifications.clear() } unseenNotifFilter.invalidateList("keyguard no longer showing") - // Keyguard is gone, notifications can be immediately marked as seen when they - // become visible. - trackSeenNotificationsWhileUnlocked() + trackUnseenNotifications() } } } - /** - * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually - * been "seen" while the device is on the keyguard. - */ - private suspend fun trackSeenNotificationsWhileLocked( - notificationsSeenWhileLocked: MutableSet, - ) = coroutineScope { - // Remove removed notifications from the set - launch { - unseenEntryRemoved.collect { entry -> - if (notificationsSeenWhileLocked.remove(entry)) { - logger.logRemoveSeenOnLockscreen(entry) + private suspend fun awaitTimeSpentNotDozing(duration: Duration) { + keyguardRepository.isDozing + // Use transformLatest so that the timeout delay is cancelled if the device enters doze, + // and is restarted when doze ends. + .transformLatest { isDozing -> + if (!isDozing) { + delay(duration) + // Signal timeout has completed + emit(Unit) } } - } - // 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) - } - } + // Suspend until the first emission + .first() } - /** - * 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 - ) = coroutineScope { - // All child tracking jobs will be cancelled automatically when this is cancelled. - val trackingJobsByEntry = mutableMapOf() - - /** - * 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 + // Track "unseen" notifications, marking them as seen when either shade is expanded or the // notification becomes heads up. - private suspend fun trackSeenNotificationsWhileUnlocked() { + private suspend fun trackUnseenNotifications() { coroutineScope { launch { clearUnseenNotificationsWhenShadeIsExpanded() } launch { markHeadsUpNotificationsAsSeen() } @@ -299,7 +250,6 @@ constructor( ) { logger.logUnseenAdded(entry.key) unseenNotifications.add(entry) - unseenEntryAdded.tryEmit(entry) } } @@ -309,14 +259,12 @@ constructor( ) { 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) } } } @@ -399,3 +347,6 @@ constructor( private val SEEN_TIMEOUT = 5.seconds } } + +private val TransitionStep.isScreenTurningOff: Boolean + get() = transitionState == TransitionState.STARTED && to != KeyguardState.GONE diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt index c612816617179..1f8ec3411bcda 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.collection.coordinator import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.UnseenNotificationLog -import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject private const val TAG = "KeyguardCoordinator" @@ -29,14 +28,11 @@ class KeyguardCoordinatorLogger constructor( @UnseenNotificationLog private val buffer: LogBuffer, ) { - fun logSeenOnLockscreen(entry: NotificationEntry) = + fun logSeenOnLockscreen() = buffer.log( TAG, LogLevel.DEBUG, - messageInitializer = { str1 = entry.key }, - messagePrinter = { - "Notification [$str1] on lockscreen will be marked as seen when unlocked." - }, + "Notifications on lockscreen will be marked as seen when unlocked." ) fun logTrackingUnseen(trackingUnseen: Boolean) = @@ -47,21 +43,11 @@ constructor( messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, ) - fun logAllMarkedSeenOnUnlock( - seenCount: Int, - remainingUnseenCount: Int, - ) = + fun logAllMarkedSeenOnUnlock() = buffer.log( TAG, LogLevel.DEBUG, - messageInitializer = { - int1 = seenCount - int2 = remainingUnseenCount - }, - messagePrinter = { - "$int1 Notifications have been marked as seen now that device is unlocked. " + - "$int2 notifications remain unseen." - }, + "Notifications have been marked as seen now that device is unlocked." ) fun logShadeExpanded() = @@ -110,60 +96,4 @@ constructor( messageInitializer = { str1 = key }, messagePrinter = { "Unseen notif has become heads up: $str1" }, ) - - fun logTrackingLockscreenSeenDuration(unseenNotifications: Set) { - buffer.log( - TAG, - LogLevel.DEBUG, - messageInitializer = { - str1 = unseenNotifications.joinToString { it.key } - int1 = unseenNotifications.size - }, - messagePrinter = { - "Tracking $int1 unseen notifications for lockscreen seen duration threshold: $str1" - }, - ) - } - - fun logTrackingLockscreenSeenDuration(entry: NotificationEntry) { - buffer.log( - TAG, - LogLevel.DEBUG, - messageInitializer = { str1 = entry.key }, - messagePrinter = { - "Tracking new notification for lockscreen seen duration threshold: $str1" - }, - ) - } - - fun logStopTrackingLockscreenSeenDuration(entry: NotificationEntry) { - buffer.log( - TAG, - LogLevel.DEBUG, - messageInitializer = { str1 = entry.key }, - messagePrinter = { - "Stop tracking removed notification for lockscreen seen duration threshold: $str1" - }, - ) - } - - fun logResetSeenOnLockscreen(entry: NotificationEntry) { - buffer.log( - TAG, - LogLevel.DEBUG, - messageInitializer = { str1 = entry.key }, - messagePrinter = { - "Reset tracking updated notification for lockscreen seen duration threshold: $str1" - }, - ) - } - - fun logRemoveSeenOnLockscreen(entry: NotificationEntry) { - buffer.log( - TAG, - LogLevel.DEBUG, - messageInitializer = { str1 = entry.key }, - messagePrinter = { "Notification marked as seen on lockscreen removed: $str1" }, - ) - } } 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 ea70e9e44c663..2fbe87158ebae 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 @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder @@ -45,14 +46,11 @@ import com.android.systemui.statusbar.notification.interruption.KeyguardNotifica 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 @@ -64,8 +62,9 @@ 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 java.util.function.Consumer +import kotlin.time.Duration.Companion.seconds import org.mockito.Mockito.`when` as whenever @SmallTest @@ -76,6 +75,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() private val keyguardRepository = FakeKeyguardRepository() private val keyguardTransitionRepository = FakeKeyguardTransitionRepository() + private val notifPipelineFlags: NotifPipelineFlags = mock() private val notifPipeline: NotifPipeline = mock() private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock() private val statusBarStateController: StatusBarStateController = mock() @@ -136,8 +136,13 @@ class KeyguardCoordinatorTest : SysuiTestCase() { ) testScheduler.runCurrent() - // THEN: We are no longer listening for shade expansions - verify(statusBarStateController, never()).addCallback(any()) + // WHEN: The shade is expanded + whenever(statusBarStateController.isExpanded).thenReturn(true) + statusBarStateListener.onExpandedChanged(true) + testScheduler.runCurrent() + + // THEN: The notification is still treated as "unseen" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() } } @@ -147,10 +152,6 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(false) runKeyguardCoordinatorTest { - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) - ) - // WHEN: A notification is posted val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) @@ -161,9 +162,6 @@ class KeyguardCoordinatorTest : SysuiTestCase() { // WHEN: The keyguard is now showing keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD) - ) testScheduler.runCurrent() // THEN: The notification is recognized as "seen" and is filtered out. @@ -171,9 +169,6 @@ class KeyguardCoordinatorTest : SysuiTestCase() { // WHEN: The keyguard goes away keyguardRepository.setKeyguardShowing(false) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.AOD, to = KeyguardState.GONE) - ) testScheduler.runCurrent() // THEN: The notification is shown regardless @@ -187,10 +182,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(true) runKeyguardCoordinatorTest { - val fakeEntry = - NotificationEntryBuilder() + val fakeEntry = NotificationEntryBuilder() .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build()) - .build() + .build() collectionListener.onEntryAdded(fakeEntry) // WHEN: The keyguard is now showing @@ -208,13 +202,11 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(true) runKeyguardCoordinatorTest { - val fakeEntry = - NotificationEntryBuilder().build().apply { - row = - mock().apply { - whenever(isMediaRow).thenReturn(true) - } + val fakeEntry = NotificationEntryBuilder().build().apply { + row = mock().apply { + whenever(isMediaRow).thenReturn(true) } + } collectionListener.onEntryAdded(fakeEntry) // WHEN: The keyguard is now showing @@ -307,12 +299,14 @@ class KeyguardCoordinatorTest : SysuiTestCase() { runKeyguardCoordinatorTest { // WHEN: A new notification is posted val fakeSummary = NotificationEntryBuilder().build() - val fakeChild = - NotificationEntryBuilder() + val fakeChild = NotificationEntryBuilder() .setGroup(context, "group") .setGroupSummary(context, false) .build() - GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build() + GroupEntryBuilder() + .setSummary(fakeSummary) + .addChild(fakeChild) + .build() collectionListener.onEntryAdded(fakeSummary) collectionListener.onEntryAdded(fakeChild) @@ -337,10 +331,6 @@ class KeyguardCoordinatorTest : SysuiTestCase() { runKeyguardCoordinatorTest { val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN) - ) - testScheduler.runCurrent() // WHEN: five seconds have passed testScheduler.advanceTimeBy(5.seconds) @@ -348,16 +338,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() { // WHEN: Keyguard is no longer showing keyguardRepository.setKeyguardShowing(false) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) - ) testScheduler.runCurrent() // WHEN: Keyguard is shown again keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD) - ) testScheduler.runCurrent() // THEN: The notification is now recognized as "seen" and is filtered out. @@ -370,17 +354,11 @@ class KeyguardCoordinatorTest : SysuiTestCase() { // GIVEN: Keyguard is showing, unseen notification is present keyguardRepository.setKeyguardShowing(true) runKeyguardCoordinatorTest { - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) - ) val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) // WHEN: Keyguard is no longer showing keyguardRepository.setKeyguardShowing(false) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) - ) // WHEN: Keyguard is shown again keyguardRepository.setKeyguardShowing(true) @@ -391,212 +369,14 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } } - @Test - fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() { - // GIVEN: Keyguard is showing, not dozing, unseen notification is present - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setIsDozing(false) - runKeyguardCoordinatorTest { - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = 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) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) - ) - testScheduler.runCurrent() - - // 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.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) - ) - 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.sendTransitionStep( - TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) - ) - 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.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) - ) - 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.sendTransitionStep( - TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) - ) - 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.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) - ) - 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.sendTransitionStep( - TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE) - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN) - ) - 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 fakeSettings = FakeSettings().apply { + putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1) + } val seenNotificationsProvider = SeenNotificationsProviderImpl() val keyguardCoordinator = KeyguardCoordinator( @@ -607,6 +387,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardRepository, keyguardTransitionRepository, mock(), + notifPipelineFlags, testScope.backgroundScope, sectionHeaderVisibilityProvider, fakeSettings, @@ -616,12 +397,11 @@ class KeyguardCoordinatorTest : SysuiTestCase() { keyguardCoordinator.attach(notifPipeline) testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { KeyguardCoordinatorTestScope( - keyguardCoordinator, - testScope, - seenNotificationsProvider, - fakeSettings, - ) - .testBlock() + keyguardCoordinator, + testScope, + seenNotificationsProvider, + fakeSettings, + ).testBlock() } } @@ -634,9 +414,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val testScheduler: TestCoroutineScheduler get() = scope.testScheduler - val onStateChangeListener: Consumer = withArgCaptor { - verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) - } + val onStateChangeListener: Consumer = + withArgCaptor { + verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) + } val unseenFilter: NotifFilter get() = keyguardCoordinator.unseenNotifFilter @@ -645,11 +426,11 @@ class KeyguardCoordinatorTest : SysuiTestCase() { verify(notifPipeline).addCollectionListener(capture()) } - val onHeadsUpChangedListener: OnHeadsUpChangedListener - get() = withArgCaptor { verify(headsUpManager).addListener(capture()) } + val onHeadsUpChangedListener: OnHeadsUpChangedListener get() = + withArgCaptor { verify(headsUpManager).addListener(capture()) } - val statusBarStateListener: StatusBarStateController.StateListener - get() = withArgCaptor { verify(statusBarStateController).addCallback(capture()) } + val statusBarStateListener: StatusBarStateController.StateListener get() = + withArgCaptor { verify(statusBarStateController).addCallback(capture()) } var showOnlyUnseenNotifsOnKeyguardSetting: Boolean get() =