1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 @file:OptIn(ExperimentalCoroutinesApi::class)
17 
18 package com.android.systemui.statusbar.notification.collection.coordinator
19 
20 import android.app.Notification
21 import android.os.UserHandle
22 import android.provider.Settings
23 import android.testing.AndroidTestingRunner
24 import androidx.test.filters.SmallTest
25 import com.android.systemui.SysuiTestCase
26 import com.android.systemui.coroutines.advanceTimeBy
27 import com.android.systemui.dump.DumpManager
28 import com.android.systemui.dump.logcatLogBuffer
29 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
30 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
31 import com.android.systemui.keyguard.shared.model.KeyguardState
32 import com.android.systemui.keyguard.shared.model.TransitionState
33 import com.android.systemui.keyguard.shared.model.TransitionStep
34 import com.android.systemui.plugins.statusbar.StatusBarStateController
35 import com.android.systemui.statusbar.StatusBarState
36 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
37 import com.android.systemui.statusbar.notification.collection.NotifPipeline
38 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
39 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
40 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
41 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
42 import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
43 import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProvider
44 import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl
45 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
46 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
47 import com.android.systemui.statusbar.policy.HeadsUpManager
48 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
49 import com.android.systemui.util.mockito.any
50 import com.android.systemui.util.mockito.eq
51 import com.android.systemui.util.mockito.mock
52 import com.android.systemui.util.mockito.withArgCaptor
53 import com.android.systemui.util.settings.FakeSettings
54 import com.google.common.truth.Truth.assertThat
55 import java.util.function.Consumer
56 import kotlin.time.Duration.Companion.seconds
57 import kotlinx.coroutines.CoroutineScope
58 import kotlinx.coroutines.ExperimentalCoroutinesApi
59 import kotlinx.coroutines.test.TestCoroutineScheduler
60 import kotlinx.coroutines.test.TestScope
61 import kotlinx.coroutines.test.UnconfinedTestDispatcher
62 import kotlinx.coroutines.test.runTest
63 import org.junit.Test
64 import org.junit.runner.RunWith
65 import org.mockito.ArgumentMatchers.same
66 import org.mockito.Mockito.anyString
67 import org.mockito.Mockito.clearInvocations
68 import org.mockito.Mockito.never
69 import org.mockito.Mockito.verify
70 import org.mockito.Mockito.`when` as whenever
71 
72 @SmallTest
73 @RunWith(AndroidTestingRunner::class)
74 class KeyguardCoordinatorTest : SysuiTestCase() {
75 
76     private val headsUpManager: HeadsUpManager = mock()
77     private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
78     private val keyguardRepository = FakeKeyguardRepository()
79     private val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
80     private val notifPipeline: NotifPipeline = mock()
81     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
82     private val statusBarStateController: StatusBarStateController = mock()
83 
84     @Test
85     fun testSetSectionHeadersVisibleInShade() = runKeyguardCoordinatorTest {
86         clearInvocations(sectionHeaderVisibilityProvider)
87         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
88         onStateChangeListener.accept("state change")
89         verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(true)
90     }
91 
92     @Test
93     fun testSetSectionHeadersNotVisibleOnKeyguard() = runKeyguardCoordinatorTest {
94         clearInvocations(sectionHeaderVisibilityProvider)
95         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
96         onStateChangeListener.accept("state change")
97         verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false)
98     }
99 
100     @Test
101     fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() {
102         // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
103         keyguardRepository.setKeyguardShowing(false)
104         whenever(statusBarStateController.isExpanded).thenReturn(true)
105         runKeyguardCoordinatorTest {
106             val fakeEntry = NotificationEntryBuilder().build()
107             collectionListener.onEntryAdded(fakeEntry)
108 
109             // WHEN: The keyguard is now showing
110             keyguardRepository.setKeyguardShowing(true)
111             testScheduler.runCurrent()
112 
113             // THEN: The notification is recognized as "seen" and is filtered out.
114             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
115 
116             // WHEN: The keyguard goes away
117             keyguardRepository.setKeyguardShowing(false)
118             testScheduler.runCurrent()
119 
120             // THEN: The notification is shown regardless
121             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
122         }
123     }
124 
125     @Test
126     fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() {
127         // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present
128         keyguardRepository.setKeyguardShowing(false)
129         whenever(statusBarStateController.isExpanded).thenReturn(false)
130         runKeyguardCoordinatorTest {
131             val fakeEntry = NotificationEntryBuilder().build()
132             collectionListener.onEntryAdded(fakeEntry)
133 
134             // WHEN: The device transitions to AOD
135             keyguardTransitionRepository.sendTransitionStep(
136                 TransitionStep(to = KeyguardState.AOD, transitionState = TransitionState.STARTED),
137             )
138             testScheduler.runCurrent()
139 
140             // THEN: We are no longer listening for shade expansions
141             verify(statusBarStateController, never()).addCallback(any())
142         }
143     }
144 
145     @Test
146     fun unseenFilter_headsUpMarkedAsSeen() {
147         // GIVEN: Keyguard is not showing, shade is not expanded
148         keyguardRepository.setKeyguardShowing(false)
149         whenever(statusBarStateController.isExpanded).thenReturn(false)
150         runKeyguardCoordinatorTest {
151             keyguardTransitionRepository.sendTransitionStep(
152                 TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
153             )
154 
155             // WHEN: A notification is posted
156             val fakeEntry = NotificationEntryBuilder().build()
157             collectionListener.onEntryAdded(fakeEntry)
158 
159             // WHEN: That notification is heads up
160             onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true)
161             testScheduler.runCurrent()
162 
163             // WHEN: The keyguard is now showing
164             keyguardRepository.setKeyguardShowing(true)
165             keyguardTransitionRepository.sendTransitionStep(
166                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
167             )
168             testScheduler.runCurrent()
169 
170             // THEN: The notification is recognized as "seen" and is filtered out.
171             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
172 
173             // WHEN: The keyguard goes away
174             keyguardRepository.setKeyguardShowing(false)
175             keyguardTransitionRepository.sendTransitionStep(
176                 TransitionStep(from = KeyguardState.AOD, to = KeyguardState.GONE)
177             )
178             testScheduler.runCurrent()
179 
180             // THEN: The notification is shown regardless
181             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
182         }
183     }
184 
185     @Test
186     fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() {
187         // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present
188         keyguardRepository.setKeyguardShowing(false)
189         whenever(statusBarStateController.isExpanded).thenReturn(true)
190         runKeyguardCoordinatorTest {
191             val fakeEntry =
192                 NotificationEntryBuilder()
193                     .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build())
194                     .build()
195             collectionListener.onEntryAdded(fakeEntry)
196 
197             // WHEN: The keyguard is now showing
198             keyguardRepository.setKeyguardShowing(true)
199             testScheduler.runCurrent()
200 
201             // THEN: The notification is recognized as "ongoing" and is not filtered out.
202             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
203         }
204     }
205 
206     @Test
207     fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() {
208         // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present
209         keyguardRepository.setKeyguardShowing(false)
210         whenever(statusBarStateController.isExpanded).thenReturn(true)
211         runKeyguardCoordinatorTest {
212             val fakeEntry =
213                 NotificationEntryBuilder().build().apply {
214                     row =
215                         mock<ExpandableNotificationRow>().apply {
216                             whenever(isMediaRow).thenReturn(true)
217                         }
218                 }
219             collectionListener.onEntryAdded(fakeEntry)
220 
221             // WHEN: The keyguard is now showing
222             keyguardRepository.setKeyguardShowing(true)
223             testScheduler.runCurrent()
224 
225             // THEN: The notification is recognized as "media" and is not filtered out.
226             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
227         }
228     }
229 
230     @Test
231     fun unseenFilterUpdatesSeenProviderWhenSuppressing() {
232         // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
233         keyguardRepository.setKeyguardShowing(false)
234         whenever(statusBarStateController.isExpanded).thenReturn(true)
235         runKeyguardCoordinatorTest {
236             val fakeEntry = NotificationEntryBuilder().build()
237             collectionListener.onEntryAdded(fakeEntry)
238 
239             // WHEN: The keyguard is now showing
240             keyguardRepository.setKeyguardShowing(true)
241             testScheduler.runCurrent()
242 
243             // THEN: The notification is recognized as "seen" and is filtered out.
244             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
245 
246             // WHEN: The filter is cleaned up
247             unseenFilter.onCleanup()
248 
249             // THEN: The SeenNotificationProvider has been updated to reflect the suppression
250             assertThat(seenNotificationsProvider.hasFilteredOutSeenNotifications).isTrue()
251         }
252     }
253 
254     @Test
255     fun unseenFilterInvalidatesWhenSettingChanges() {
256         // GIVEN: Keyguard is not showing, and shade is expanded
257         keyguardRepository.setKeyguardShowing(false)
258         whenever(statusBarStateController.isExpanded).thenReturn(true)
259         runKeyguardCoordinatorTest {
260             // GIVEN: A notification is present
261             val fakeEntry = NotificationEntryBuilder().build()
262             collectionListener.onEntryAdded(fakeEntry)
263 
264             // GIVEN: The setting for filtering unseen notifications is disabled
265             showOnlyUnseenNotifsOnKeyguardSetting = false
266 
267             // GIVEN: The pipeline has registered the unseen filter for invalidation
268             val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock()
269             unseenFilter.setInvalidationListener(invalidationListener)
270 
271             // WHEN: The keyguard is now showing
272             keyguardRepository.setKeyguardShowing(true)
273             testScheduler.runCurrent()
274 
275             // THEN: The notification is not filtered out
276             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
277 
278             // WHEN: The secure setting is changed
279             showOnlyUnseenNotifsOnKeyguardSetting = true
280 
281             // THEN: The pipeline is invalidated
282             verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), anyString())
283 
284             // THEN: The notification is recognized as "seen" and is filtered out.
285             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
286         }
287     }
288 
289     @Test
290     fun unseenFilterAllowsNewNotif() {
291         // GIVEN: Keyguard is showing, no notifications present
292         keyguardRepository.setKeyguardShowing(true)
293         runKeyguardCoordinatorTest {
294             // WHEN: A new notification is posted
295             val fakeEntry = NotificationEntryBuilder().build()
296             collectionListener.onEntryAdded(fakeEntry)
297 
298             // THEN: The notification is recognized as "unseen" and is not filtered out.
299             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
300         }
301     }
302 
303     @Test
304     fun unseenFilterSeenGroupSummaryWithUnseenChild() {
305         // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
306         keyguardRepository.setKeyguardShowing(false)
307         whenever(statusBarStateController.isExpanded).thenReturn(true)
308         runKeyguardCoordinatorTest {
309             // WHEN: A new notification is posted
310             val fakeSummary = NotificationEntryBuilder().build()
311             val fakeChild =
312                 NotificationEntryBuilder()
313                     .setGroup(context, "group")
314                     .setGroupSummary(context, false)
315                     .build()
316             GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build()
317 
318             collectionListener.onEntryAdded(fakeSummary)
319             collectionListener.onEntryAdded(fakeChild)
320 
321             // WHEN: Keyguard is now showing, both notifications are marked as seen
322             keyguardRepository.setKeyguardShowing(true)
323             testScheduler.runCurrent()
324 
325             // WHEN: The child notification is now unseen
326             collectionListener.onEntryUpdated(fakeChild)
327 
328             // THEN: The summary is not filtered out, because the child is unseen
329             assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse()
330         }
331     }
332 
333     @Test
334     fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() {
335         // GIVEN: Keyguard is showing, not dozing, unseen notification is present
336         keyguardRepository.setKeyguardShowing(true)
337         keyguardRepository.setIsDozing(false)
338         runKeyguardCoordinatorTest {
339             val fakeEntry = NotificationEntryBuilder().build()
340             collectionListener.onEntryAdded(fakeEntry)
341             keyguardTransitionRepository.sendTransitionStep(
342                 TransitionStep(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN)
343             )
344             testScheduler.runCurrent()
345 
346             // WHEN: five seconds have passed
347             testScheduler.advanceTimeBy(5.seconds)
348             testScheduler.runCurrent()
349 
350             // WHEN: Keyguard is no longer showing
351             keyguardRepository.setKeyguardShowing(false)
352             keyguardTransitionRepository.sendTransitionStep(
353                 TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
354             )
355             testScheduler.runCurrent()
356 
357             // WHEN: Keyguard is shown again
358             keyguardRepository.setKeyguardShowing(true)
359             keyguardTransitionRepository.sendTransitionStep(
360                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
361             )
362             testScheduler.runCurrent()
363 
364             // THEN: The notification is now recognized as "seen" and is filtered out.
365             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
366         }
367     }
368 
369     @Test
370     fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() {
371         // GIVEN: Keyguard is showing, unseen notification is present
372         keyguardRepository.setKeyguardShowing(true)
373         runKeyguardCoordinatorTest {
374             keyguardTransitionRepository.sendTransitionStep(
375                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
376             )
377             val fakeEntry = NotificationEntryBuilder().build()
378             collectionListener.onEntryAdded(fakeEntry)
379 
380             // WHEN: Keyguard is no longer showing
381             keyguardRepository.setKeyguardShowing(false)
382             keyguardTransitionRepository.sendTransitionStep(
383                 TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
384             )
385 
386             // WHEN: Keyguard is shown again
387             keyguardRepository.setKeyguardShowing(true)
388             testScheduler.runCurrent()
389 
390             // THEN: The notification is not recognized as "seen" and is not filtered out.
391             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
392         }
393     }
394 
395     @Test
396     fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() {
397         // GIVEN: Keyguard is showing, not dozing, unseen notification is present
398         keyguardRepository.setKeyguardShowing(true)
399         keyguardRepository.setIsDozing(false)
400         runKeyguardCoordinatorTest {
401             keyguardTransitionRepository.sendTransitionStep(
402                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
403             )
404             val firstEntry = NotificationEntryBuilder().setId(1).build()
405             collectionListener.onEntryAdded(firstEntry)
406             testScheduler.runCurrent()
407 
408             // WHEN: one second has passed
409             testScheduler.advanceTimeBy(1.seconds)
410             testScheduler.runCurrent()
411 
412             // WHEN: another unseen notification is posted
413             val secondEntry = NotificationEntryBuilder().setId(2).build()
414             collectionListener.onEntryAdded(secondEntry)
415             testScheduler.runCurrent()
416 
417             // WHEN: four more seconds have passed
418             testScheduler.advanceTimeBy(4.seconds)
419             testScheduler.runCurrent()
420 
421             // WHEN: the keyguard is no longer showing
422             keyguardRepository.setKeyguardShowing(false)
423             keyguardTransitionRepository.sendTransitionStep(
424                 TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
425             )
426             testScheduler.runCurrent()
427 
428             // WHEN: Keyguard is shown again
429             keyguardRepository.setKeyguardShowing(true)
430             keyguardTransitionRepository.sendTransitionStep(
431                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
432             )
433             testScheduler.runCurrent()
434 
435             // THEN: The first notification is considered seen and is filtered out.
436             assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue()
437 
438             // THEN: The second notification is still considered unseen and is not filtered out
439             assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse()
440         }
441     }
442 
443     @Test
444     fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() {
445         // GIVEN: Keyguard is showing, not dozing
446         keyguardRepository.setKeyguardShowing(true)
447         keyguardRepository.setIsDozing(false)
448         runKeyguardCoordinatorTest {
449             keyguardTransitionRepository.sendTransitionStep(
450                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
451             )
452             testScheduler.runCurrent()
453 
454             // WHEN: a new notification is posted
455             val entry = NotificationEntryBuilder().setId(1).build()
456             collectionListener.onEntryAdded(entry)
457             testScheduler.runCurrent()
458 
459             // WHEN: five more seconds have passed
460             testScheduler.advanceTimeBy(5.seconds)
461             testScheduler.runCurrent()
462 
463             // WHEN: the notification is removed
464             collectionListener.onEntryRemoved(entry, 0)
465             testScheduler.runCurrent()
466 
467             // WHEN: the notification is re-posted
468             collectionListener.onEntryAdded(entry)
469             testScheduler.runCurrent()
470 
471             // WHEN: one more second has passed
472             testScheduler.advanceTimeBy(1.seconds)
473             testScheduler.runCurrent()
474 
475             // WHEN: the keyguard is no longer showing
476             keyguardRepository.setKeyguardShowing(false)
477             keyguardTransitionRepository.sendTransitionStep(
478                 TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
479             )
480             testScheduler.runCurrent()
481 
482             // WHEN: Keyguard is shown again
483             keyguardRepository.setKeyguardShowing(true)
484             keyguardTransitionRepository.sendTransitionStep(
485                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
486             )
487             testScheduler.runCurrent()
488 
489             // THEN: The notification is considered unseen and is not filtered out.
490             assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
491         }
492     }
493 
494     @Test
495     fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() {
496         // GIVEN: Keyguard is showing, not dozing
497         keyguardRepository.setKeyguardShowing(true)
498         keyguardRepository.setIsDozing(false)
499         runKeyguardCoordinatorTest {
500             keyguardTransitionRepository.sendTransitionStep(
501                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
502             )
503             testScheduler.runCurrent()
504 
505             // WHEN: a new notification is posted
506             val entry = NotificationEntryBuilder().setId(1).build()
507             collectionListener.onEntryAdded(entry)
508             testScheduler.runCurrent()
509 
510             // WHEN: one second has passed
511             testScheduler.advanceTimeBy(1.seconds)
512             testScheduler.runCurrent()
513 
514             // WHEN: the notification is removed
515             collectionListener.onEntryRemoved(entry, 0)
516             testScheduler.runCurrent()
517 
518             // WHEN: the notification is re-posted
519             collectionListener.onEntryAdded(entry)
520             testScheduler.runCurrent()
521 
522             // WHEN: one more second has passed
523             testScheduler.advanceTimeBy(1.seconds)
524             testScheduler.runCurrent()
525 
526             // WHEN: the keyguard is no longer showing
527             keyguardRepository.setKeyguardShowing(false)
528             keyguardTransitionRepository.sendTransitionStep(
529                 TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
530             )
531             testScheduler.runCurrent()
532 
533             // WHEN: Keyguard is shown again
534             keyguardRepository.setKeyguardShowing(true)
535             keyguardTransitionRepository.sendTransitionStep(
536                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
537             )
538             testScheduler.runCurrent()
539 
540             // THEN: The notification is considered unseen and is not filtered out.
541             assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
542         }
543     }
544 
545     @Test
546     fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() {
547         // GIVEN: Keyguard is showing, not dozing
548         keyguardRepository.setKeyguardShowing(true)
549         keyguardRepository.setIsDozing(false)
550         runKeyguardCoordinatorTest {
551             keyguardTransitionRepository.sendTransitionStep(
552                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
553             )
554             testScheduler.runCurrent()
555 
556             // WHEN: a new notification is posted
557             val entry = NotificationEntryBuilder().setId(1).build()
558             collectionListener.onEntryAdded(entry)
559             testScheduler.runCurrent()
560 
561             // WHEN: one second has passed
562             testScheduler.advanceTimeBy(1.seconds)
563             testScheduler.runCurrent()
564 
565             // WHEN: the notification is updated
566             collectionListener.onEntryUpdated(entry)
567             testScheduler.runCurrent()
568 
569             // WHEN: four more seconds have passed
570             testScheduler.advanceTimeBy(4.seconds)
571             testScheduler.runCurrent()
572 
573             // WHEN: the keyguard is no longer showing
574             keyguardRepository.setKeyguardShowing(false)
575             keyguardTransitionRepository.sendTransitionStep(
576                 TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
577             )
578             testScheduler.runCurrent()
579 
580             // WHEN: Keyguard is shown again
581             keyguardRepository.setKeyguardShowing(true)
582             keyguardTransitionRepository.sendTransitionStep(
583                 TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
584             )
585             testScheduler.runCurrent()
586 
587             // THEN: The notification is considered unseen and is not filtered out.
588             assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
589         }
590     }
591 
592     private fun runKeyguardCoordinatorTest(
593         testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
594     ) {
595         val testDispatcher = UnconfinedTestDispatcher()
596         val testScope = TestScope(testDispatcher)
597         val fakeSettings =
598             FakeSettings().apply {
599                 putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
600             }
601         val seenNotificationsProvider = SeenNotificationsProviderImpl()
602         val keyguardCoordinator =
603             KeyguardCoordinator(
604                 testDispatcher,
605                 mock<DumpManager>(),
606                 headsUpManager,
607                 keyguardNotifVisibilityProvider,
608                 keyguardRepository,
609                 keyguardTransitionRepository,
610                 KeyguardCoordinatorLogger(logcatLogBuffer()),
611                 testScope.backgroundScope,
612                 sectionHeaderVisibilityProvider,
613                 fakeSettings,
614                 seenNotificationsProvider,
615                 statusBarStateController,
616             )
617         keyguardCoordinator.attach(notifPipeline)
618         testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
619             KeyguardCoordinatorTestScope(
620                     keyguardCoordinator,
621                     testScope,
622                     seenNotificationsProvider,
623                     fakeSettings,
624                 )
625                 .testBlock()
626         }
627     }
628 
629     private inner class KeyguardCoordinatorTestScope(
630         private val keyguardCoordinator: KeyguardCoordinator,
631         private val scope: TestScope,
632         val seenNotificationsProvider: SeenNotificationsProvider,
633         private val fakeSettings: FakeSettings,
634     ) : CoroutineScope by scope {
635         val testScheduler: TestCoroutineScheduler
636             get() = scope.testScheduler
637 
638         val onStateChangeListener: Consumer<String> = withArgCaptor {
639             verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
640         }
641 
642         val unseenFilter: NotifFilter
643             get() = keyguardCoordinator.unseenNotifFilter
644 
645         val collectionListener: NotifCollectionListener = withArgCaptor {
646             verify(notifPipeline).addCollectionListener(capture())
647         }
648 
649         val onHeadsUpChangedListener: OnHeadsUpChangedListener
650             get() = withArgCaptor { verify(headsUpManager).addListener(capture()) }
651 
652         val statusBarStateListener: StatusBarStateController.StateListener
653             get() = withArgCaptor { verify(statusBarStateController).addCallback(capture()) }
654 
655         var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
656             get() =
657                 fakeSettings.getIntForUser(
658                     Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
659                     UserHandle.USER_CURRENT,
660                 ) == 1
661             set(value) {
662                 fakeSettings.putIntForUser(
663                     Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
664                     if (value) 1 else 2,
665                     UserHandle.USER_CURRENT,
666                 )
667             }
668     }
669 }
670