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