• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.systemui.statusbar.notification.collection.coordinator
17 
18 import android.app.Notification
19 import android.app.Notification.MediaStyle
20 import android.media.session.MediaSession
21 import android.platform.test.flag.junit.FlagsParameterization
22 import android.provider.Settings
23 import androidx.test.filters.SmallTest
24 import com.android.systemui.SysuiTestCase
25 import com.android.systemui.dump.dumpManager
26 import com.android.systemui.flags.andSceneContainer
27 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
28 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
29 import com.android.systemui.keyguard.data.repository.keyguardRepository
30 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
31 import com.android.systemui.keyguard.shared.model.KeyguardState
32 import com.android.systemui.keyguard.shared.model.TransitionStep
33 import com.android.systemui.kosmos.testDispatcher
34 import com.android.systemui.kosmos.testScope
35 import com.android.systemui.log.logcatLogBuffer
36 import com.android.systemui.plugins.statusbar.statusBarStateController
37 import com.android.systemui.scene.data.repository.Idle
38 import com.android.systemui.scene.data.repository.setTransition
39 import com.android.systemui.scene.domain.interactor.sceneInteractor
40 import com.android.systemui.scene.shared.model.Scenes
41 import com.android.systemui.statusbar.SbnBuilder
42 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
43 import com.android.systemui.statusbar.notification.collection.NotifPipeline
44 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
45 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
46 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
47 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
48 import com.android.systemui.statusbar.notification.collection.notifcollection.UpdateSource
49 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
50 import com.android.systemui.statusbar.notification.domain.interactor.lockScreenShowOnlyUnseenNotificationsSetting
51 import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
52 import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener
53 import com.android.systemui.statusbar.notification.headsup.mockHeadsUpManager
54 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
55 import com.android.systemui.testKosmos
56 import com.android.systemui.util.settings.FakeSettings
57 import com.android.systemui.util.settings.fakeSettings
58 import com.google.common.truth.Truth.assertThat
59 import kotlin.time.Duration.Companion.seconds
60 import kotlinx.coroutines.CoroutineScope
61 import kotlinx.coroutines.test.TestCoroutineScheduler
62 import kotlinx.coroutines.test.TestScope
63 import kotlinx.coroutines.test.UnconfinedTestDispatcher
64 import kotlinx.coroutines.test.runTest
65 import org.junit.Test
66 import org.junit.runner.RunWith
67 import org.mockito.ArgumentMatchers.same
68 import org.mockito.kotlin.any
69 import org.mockito.kotlin.argumentCaptor
70 import org.mockito.kotlin.mock
71 import org.mockito.kotlin.never
72 import org.mockito.kotlin.verify
73 import org.mockito.kotlin.whenever
74 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
75 import platform.test.runner.parameterized.Parameters
76 
77 @SmallTest
78 @RunWith(ParameterizedAndroidJunit4::class)
79 class OriginalUnseenKeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() {
80 
81     private val kosmos =
<lambda>null82         testKosmos().apply {
83             testDispatcher = UnconfinedTestDispatcher()
84             statusBarStateController = mock()
85             fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
86         }
87 
88     private val keyguardRepository
89         get() = kosmos.fakeKeyguardRepository
90 
91     private val keyguardTransitionRepository
92         get() = kosmos.fakeKeyguardTransitionRepository
93 
94     private val statusBarStateController
95         get() = kosmos.statusBarStateController
96 
97     private val notifPipeline: NotifPipeline = mock()
98 
99     init {
100         mSetFlagsRule.setFlagsParameterization(flags)
101     }
102 
103     @Test
unseenFilterSuppressesSeenNotifWhileKeyguardShowingnull104     fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() {
105         // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
106         keyguardRepository.setKeyguardShowing(false)
107         whenever(statusBarStateController.isExpanded).thenReturn(true)
108         runKeyguardCoordinatorTest {
109             val fakeEntry = NotificationEntryBuilder().build()
110             collectionListener.onEntryAdded(fakeEntry)
111 
112             // WHEN: The keyguard is now showing
113             keyguardRepository.setKeyguardShowing(true)
114             testScheduler.runCurrent()
115 
116             // THEN: The notification is recognized as "seen" and is filtered out.
117             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
118 
119             // WHEN: The keyguard goes away
120             keyguardRepository.setKeyguardShowing(false)
121             testScheduler.runCurrent()
122 
123             // THEN: The notification is shown regardless
124             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
125         }
126     }
127 
128     @Test
unseenFilterStopsMarkingSeenNotifWhenTransitionToAodnull129     fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() {
130         // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present
131         keyguardRepository.setKeyguardShowing(false)
132         whenever(statusBarStateController.isExpanded).thenReturn(false)
133         runKeyguardCoordinatorTest {
134             val fakeEntry = NotificationEntryBuilder().build()
135             collectionListener.onEntryAdded(fakeEntry)
136 
137             // WHEN: The device transitions to AOD
138             keyguardTransitionRepository.sendTransitionSteps(
139                 from = KeyguardState.GONE,
140                 to = KeyguardState.AOD,
141                 this.testScheduler,
142             )
143             testScheduler.runCurrent()
144 
145             // THEN: We are no longer listening for shade expansions
146             verify(statusBarStateController, never()).addCallback(any())
147         }
148     }
149 
150     @Test
unseenFilter_headsUpMarkedAsSeennull151     fun unseenFilter_headsUpMarkedAsSeen() {
152         // GIVEN: Keyguard is not showing, shade is not expanded
153         keyguardRepository.setKeyguardShowing(false)
154         whenever(statusBarStateController.isExpanded).thenReturn(false)
155         runKeyguardCoordinatorTest {
156             kosmos.setTransition(
157                 sceneTransition = Idle(Scenes.Gone),
158                 stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE),
159             )
160 
161             // WHEN: A notification is posted
162             val fakeEntry = NotificationEntryBuilder().build()
163             collectionListener.onEntryAdded(fakeEntry)
164 
165             // WHEN: That notification is heads up
166             onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true)
167             testScheduler.runCurrent()
168 
169             // WHEN: The keyguard is now showing
170             keyguardRepository.setKeyguardShowing(true)
171             kosmos.setTransition(
172                 sceneTransition = Idle(Scenes.Lockscreen),
173                 stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD),
174             )
175 
176             // THEN: The notification is recognized as "seen" and is filtered out.
177             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
178 
179             // WHEN: The keyguard goes away
180             keyguardRepository.setKeyguardShowing(false)
181             kosmos.setTransition(
182                 sceneTransition = Idle(Scenes.Gone),
183                 stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE),
184             )
185 
186             // THEN: The notification is shown regardless
187             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
188         }
189     }
190 
191     @Test
unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowingnull192     fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() {
193         // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present
194         keyguardRepository.setKeyguardShowing(false)
195         whenever(statusBarStateController.isExpanded).thenReturn(true)
196         runKeyguardCoordinatorTest {
197             val fakeEntry =
198                 NotificationEntryBuilder()
199                     .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build())
200                     .build()
201             collectionListener.onEntryAdded(fakeEntry)
202 
203             // WHEN: The keyguard is now showing
204             keyguardRepository.setKeyguardShowing(true)
205             testScheduler.runCurrent()
206 
207             // THEN: The notification is recognized as "ongoing" and is not filtered out.
208             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
209         }
210     }
211 
212     @Test
unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowingnull213     fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() {
214         // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present
215         keyguardRepository.setKeyguardShowing(false)
216         whenever(statusBarStateController.isExpanded).thenReturn(true)
217         runKeyguardCoordinatorTest {
218             val fakeEntry =
219                 NotificationEntryBuilder().build().apply {
220                     row =
221                         mock<ExpandableNotificationRow>().apply {
222                             whenever(isMediaRow).thenReturn(true)
223                         }
224                     sbn =
225                         SbnBuilder()
226                             .setNotification(
227                                 Notification.Builder(context, "channel")
228                                     .setStyle(
229                                         MediaStyle()
230                                             .setMediaSession(
231                                                 MediaSession(context, "tag").sessionToken
232                                             )
233                                     )
234                                     .build()
235                             )
236                             .build()
237                 }
238             collectionListener.onEntryAdded(fakeEntry)
239 
240             // WHEN: The keyguard is now showing
241             keyguardRepository.setKeyguardShowing(true)
242             testScheduler.runCurrent()
243 
244             // THEN: The notification is recognized as "media" and is not filtered out.
245             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
246         }
247     }
248 
249     @Test
unseenFilterUpdatesSeenProviderWhenSuppressingnull250     fun unseenFilterUpdatesSeenProviderWhenSuppressing() {
251         // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
252         keyguardRepository.setKeyguardShowing(false)
253         whenever(statusBarStateController.isExpanded).thenReturn(true)
254         runKeyguardCoordinatorTest {
255             val fakeEntry = NotificationEntryBuilder().build()
256             collectionListener.onEntryAdded(fakeEntry)
257 
258             // WHEN: The keyguard is now showing
259             keyguardRepository.setKeyguardShowing(true)
260             testScheduler.runCurrent()
261 
262             // THEN: The notification is recognized as "seen" and is filtered out.
263             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
264 
265             // WHEN: The filter is cleaned up
266             unseenFilter.onCleanup()
267 
268             // THEN: The SeenNotificationProvider has been updated to reflect the suppression
269             assertThat(seenNotificationsInteractor.hasFilteredOutSeenNotifications.value).isTrue()
270         }
271     }
272 
273     @Test
unseenFilterInvalidatesWhenSettingChangesnull274     fun unseenFilterInvalidatesWhenSettingChanges() {
275         // GIVEN: Keyguard is not showing, and shade is expanded
276         keyguardRepository.setKeyguardShowing(false)
277         whenever(statusBarStateController.isExpanded).thenReturn(true)
278         runKeyguardCoordinatorTest {
279             // GIVEN: A notification is present
280             val fakeEntry = NotificationEntryBuilder().build()
281             collectionListener.onEntryAdded(fakeEntry)
282 
283             // GIVEN: The setting for filtering unseen notifications is disabled
284             kosmos.lockScreenShowOnlyUnseenNotificationsSetting = false
285 
286             // GIVEN: The pipeline has registered the unseen filter for invalidation
287             val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock()
288             unseenFilter.setInvalidationListener(invalidationListener)
289 
290             // WHEN: The keyguard is now showing
291             keyguardRepository.setKeyguardShowing(true)
292             testScheduler.runCurrent()
293 
294             // THEN: The notification is not filtered out
295             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
296 
297             // WHEN: The secure setting is changed
298             kosmos.lockScreenShowOnlyUnseenNotificationsSetting = true
299 
300             // THEN: The pipeline is invalidated
301             verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), any())
302 
303             // THEN: The notification is recognized as "seen" and is filtered out.
304             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
305         }
306     }
307 
308     @Test
unseenFilterAllowsNewNotifnull309     fun unseenFilterAllowsNewNotif() {
310         // GIVEN: Keyguard is showing, no notifications present
311         keyguardRepository.setKeyguardShowing(true)
312         runKeyguardCoordinatorTest {
313             // WHEN: A new notification is posted
314             val fakeEntry = NotificationEntryBuilder().build()
315             collectionListener.onEntryAdded(fakeEntry)
316 
317             // THEN: The notification is recognized as "unseen" and is not filtered out.
318             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
319         }
320     }
321 
322     @Test
unseenFilterSeenGroupSummaryWithUnseenChildnull323     fun unseenFilterSeenGroupSummaryWithUnseenChild() {
324         // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
325         keyguardRepository.setKeyguardShowing(false)
326         whenever(statusBarStateController.isExpanded).thenReturn(true)
327         runKeyguardCoordinatorTest {
328             // WHEN: A new notification is posted
329             val fakeSummary = NotificationEntryBuilder().build()
330             val fakeChild =
331                 NotificationEntryBuilder()
332                     .setGroup(context, "group")
333                     .setGroupSummary(context, false)
334                     .build()
335             GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build()
336 
337             collectionListener.onEntryAdded(fakeSummary)
338             collectionListener.onEntryAdded(fakeChild)
339 
340             // WHEN: Keyguard is now showing, both notifications are marked as seen
341             keyguardRepository.setKeyguardShowing(true)
342             testScheduler.runCurrent()
343 
344             // WHEN: The child notification is now unseen
345             collectionListener.onEntryUpdated(fakeChild)
346 
347             // THEN: The summary is not filtered out, because the child is unseen
348             assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse()
349         }
350     }
351 
352     @Test
unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAwaynull353     fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() {
354         // GIVEN: Keyguard is showing, not dozing, unseen notification is present
355         keyguardRepository.setKeyguardShowing(true)
356         keyguardRepository.setIsDozing(false)
357         runKeyguardCoordinatorTest {
358             val fakeEntry = NotificationEntryBuilder().build()
359             collectionListener.onEntryAdded(fakeEntry)
360             keyguardTransitionRepository.sendTransitionSteps(
361                 from = KeyguardState.AOD,
362                 to = KeyguardState.LOCKSCREEN,
363                 this.testScheduler,
364             )
365             testScheduler.runCurrent()
366 
367             // WHEN: five seconds have passed
368             testScheduler.advanceTimeBy(5.seconds)
369             testScheduler.runCurrent()
370 
371             // WHEN: Keyguard is no longer showing
372             keyguardRepository.setKeyguardShowing(false)
373             kosmos.setTransition(
374                 sceneTransition = Idle(Scenes.Gone),
375                 stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE),
376             )
377 
378             // WHEN: Keyguard is shown again
379             keyguardRepository.setKeyguardShowing(true)
380             kosmos.setTransition(
381                 sceneTransition = Idle(Scenes.Lockscreen),
382                 stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD),
383             )
384 
385             // THEN: The notification is now recognized as "seen" and is filtered out.
386             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
387         }
388     }
389 
390     @Test
unseenNotificationIsNotMarkedAsSeenIfShadeNotExpandednull391     fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() {
392         // GIVEN: Keyguard is showing, unseen notification is present
393         keyguardRepository.setKeyguardShowing(true)
394         runKeyguardCoordinatorTest {
395             keyguardTransitionRepository.sendTransitionSteps(
396                 from = KeyguardState.GONE,
397                 to = KeyguardState.LOCKSCREEN,
398                 this.testScheduler,
399             )
400             val fakeEntry = NotificationEntryBuilder().build()
401             collectionListener.onEntryAdded(fakeEntry)
402 
403             // WHEN: Keyguard is no longer showing
404             keyguardRepository.setKeyguardShowing(false)
405             keyguardTransitionRepository.sendTransitionSteps(
406                 from = KeyguardState.LOCKSCREEN,
407                 to = KeyguardState.GONE,
408                 this.testScheduler,
409             )
410 
411             // WHEN: Keyguard is shown again
412             keyguardRepository.setKeyguardShowing(true)
413             testScheduler.runCurrent()
414 
415             // THEN: The notification is not recognized as "seen" and is not filtered out.
416             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
417         }
418     }
419 
420     @Test
unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnoughnull421     fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() {
422         // GIVEN: Keyguard is showing, not dozing, unseen notification is present
423         keyguardRepository.setKeyguardShowing(true)
424         keyguardRepository.setIsDozing(false)
425         runKeyguardCoordinatorTest {
426             kosmos.setTransition(
427                 sceneTransition = Idle(Scenes.Lockscreen),
428                 stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN),
429             )
430             val firstEntry = NotificationEntryBuilder().setId(1).build()
431             collectionListener.onEntryAdded(firstEntry)
432             testScheduler.runCurrent()
433 
434             // WHEN: one second has passed
435             testScheduler.advanceTimeBy(1.seconds)
436             testScheduler.runCurrent()
437 
438             // WHEN: another unseen notification is posted
439             val secondEntry = NotificationEntryBuilder().setId(2).build()
440             collectionListener.onEntryAdded(secondEntry)
441             testScheduler.runCurrent()
442 
443             // WHEN: four more seconds have passed
444             testScheduler.advanceTimeBy(4.seconds)
445             testScheduler.runCurrent()
446 
447             // WHEN: the keyguard is no longer showing
448             keyguardRepository.setKeyguardShowing(false)
449             kosmos.setTransition(
450                 sceneTransition = Idle(Scenes.Gone),
451                 stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE),
452             )
453 
454             // WHEN: Keyguard is shown again
455             keyguardRepository.setKeyguardShowing(true)
456             kosmos.setTransition(
457                 sceneTransition = Idle(Scenes.Lockscreen),
458                 stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN),
459             )
460 
461             // THEN: The first notification is considered seen and is filtered out.
462             assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue()
463 
464             // THEN: The second notification is still considered unseen and is not filtered out
465             assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse()
466         }
467     }
468 
469     @Test
unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThresholdnull470     fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() {
471         // GIVEN: Keyguard is showing, not dozing
472         keyguardRepository.setKeyguardShowing(true)
473         keyguardRepository.setIsDozing(false)
474         runKeyguardCoordinatorTest {
475             keyguardTransitionRepository.sendTransitionSteps(
476                 from = KeyguardState.GONE,
477                 to = KeyguardState.LOCKSCREEN,
478                 this.testScheduler,
479             )
480             testScheduler.runCurrent()
481 
482             // WHEN: a new notification is posted
483             val entry = NotificationEntryBuilder().setId(1).build()
484             collectionListener.onEntryAdded(entry)
485             testScheduler.runCurrent()
486 
487             // WHEN: five more seconds have passed
488             testScheduler.advanceTimeBy(5.seconds)
489             testScheduler.runCurrent()
490 
491             // WHEN: the notification is removed
492             collectionListener.onEntryRemoved(entry, 0)
493             testScheduler.runCurrent()
494 
495             // WHEN: the notification is re-posted
496             collectionListener.onEntryAdded(entry)
497             testScheduler.runCurrent()
498 
499             // WHEN: one more second has passed
500             testScheduler.advanceTimeBy(1.seconds)
501             testScheduler.runCurrent()
502 
503             // WHEN: the keyguard is no longer showing
504             keyguardRepository.setKeyguardShowing(false)
505             keyguardTransitionRepository.sendTransitionSteps(
506                 from = KeyguardState.LOCKSCREEN,
507                 to = KeyguardState.GONE,
508                 this.testScheduler,
509             )
510             testScheduler.runCurrent()
511 
512             // WHEN: Keyguard is shown again
513             keyguardRepository.setKeyguardShowing(true)
514             keyguardTransitionRepository.sendTransitionSteps(
515                 from = KeyguardState.GONE,
516                 to = KeyguardState.LOCKSCREEN,
517                 this.testScheduler,
518             )
519             testScheduler.runCurrent()
520 
521             // THEN: The notification is considered unseen and is not filtered out.
522             assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
523         }
524     }
525 
526     @Test
unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThresholdnull527     fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() {
528         // GIVEN: Keyguard is showing, not dozing
529         keyguardRepository.setKeyguardShowing(true)
530         keyguardRepository.setIsDozing(false)
531         runKeyguardCoordinatorTest {
532             keyguardTransitionRepository.sendTransitionSteps(
533                 from = KeyguardState.GONE,
534                 to = KeyguardState.LOCKSCREEN,
535                 this.testScheduler,
536             )
537             testScheduler.runCurrent()
538 
539             // WHEN: a new notification is posted
540             val entry = NotificationEntryBuilder().setId(1).build()
541             collectionListener.onEntryAdded(entry)
542             testScheduler.runCurrent()
543 
544             // WHEN: one second has passed
545             testScheduler.advanceTimeBy(1.seconds)
546             testScheduler.runCurrent()
547 
548             // WHEN: the notification is removed
549             collectionListener.onEntryRemoved(entry, 0)
550             testScheduler.runCurrent()
551 
552             // WHEN: the notification is re-posted
553             collectionListener.onEntryAdded(entry)
554             testScheduler.runCurrent()
555 
556             // WHEN: one more second has passed
557             testScheduler.advanceTimeBy(1.seconds)
558             testScheduler.runCurrent()
559 
560             // WHEN: the keyguard is no longer showing
561             keyguardRepository.setKeyguardShowing(false)
562             keyguardTransitionRepository.sendTransitionSteps(
563                 from = KeyguardState.LOCKSCREEN,
564                 to = KeyguardState.GONE,
565                 this.testScheduler,
566             )
567             testScheduler.runCurrent()
568 
569             // WHEN: Keyguard is shown again
570             keyguardRepository.setKeyguardShowing(true)
571             keyguardTransitionRepository.sendTransitionSteps(
572                 from = KeyguardState.GONE,
573                 to = KeyguardState.LOCKSCREEN,
574                 this.testScheduler,
575             )
576             testScheduler.runCurrent()
577 
578             // THEN: The notification is considered unseen and is not filtered out.
579             assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
580         }
581     }
582 
583     @Test
unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThresholdnull584     fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() {
585         // GIVEN: Keyguard is showing, not dozing
586         keyguardRepository.setKeyguardShowing(true)
587         keyguardRepository.setIsDozing(false)
588         runKeyguardCoordinatorTest {
589             keyguardTransitionRepository.sendTransitionSteps(
590                 from = KeyguardState.GONE,
591                 to = KeyguardState.LOCKSCREEN,
592                 this.testScheduler,
593             )
594             testScheduler.runCurrent()
595 
596             // WHEN: a new notification is posted
597             val entry = NotificationEntryBuilder().setId(1).build()
598             collectionListener.onEntryAdded(entry)
599             testScheduler.runCurrent()
600 
601             // WHEN: one second has passed
602             testScheduler.advanceTimeBy(1.seconds)
603             testScheduler.runCurrent()
604 
605             // WHEN: the notification is updated
606             collectionListener.onEntryUpdated(entry, UpdateSource.App)
607             testScheduler.runCurrent()
608 
609             // WHEN: four more seconds have passed
610             testScheduler.advanceTimeBy(4.seconds)
611             testScheduler.runCurrent()
612 
613             // WHEN: the keyguard is no longer showing
614             keyguardRepository.setKeyguardShowing(false)
615             keyguardTransitionRepository.sendTransitionSteps(
616                 from = KeyguardState.LOCKSCREEN,
617                 to = KeyguardState.GONE,
618                 this.testScheduler,
619             )
620             testScheduler.runCurrent()
621 
622             // WHEN: Keyguard is shown again
623             keyguardRepository.setKeyguardShowing(true)
624             keyguardTransitionRepository.sendTransitionSteps(
625                 from = KeyguardState.GONE,
626                 to = KeyguardState.LOCKSCREEN,
627                 this.testScheduler,
628             )
629             testScheduler.runCurrent()
630 
631             // THEN: The notification is considered unseen and is not filtered out.
632             assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
633         }
634     }
635 
636     @Test
seenNotificationOnKeyguardMarkedAsSeenIfUpdatedBySystemServernull637     fun seenNotificationOnKeyguardMarkedAsSeenIfUpdatedBySystemServer() {
638         // GIVEN: Keyguard is showing, not dozing, unseen notification is present
639         keyguardRepository.setKeyguardShowing(true)
640         keyguardRepository.setIsDozing(false)
641         runKeyguardCoordinatorTest {
642             val fakeEntry = NotificationEntryBuilder().build()
643             collectionListener.onEntryAdded(fakeEntry)
644             keyguardTransitionRepository.sendTransitionSteps(
645                 from = KeyguardState.AOD,
646                 to = KeyguardState.LOCKSCREEN,
647                 this.testScheduler,
648             )
649             testScheduler.runCurrent()
650 
651             // WHEN: five seconds have passed
652             testScheduler.advanceTimeBy(5.seconds)
653             testScheduler.runCurrent()
654 
655             // WHEN: Keyguard is no longer showing
656             keyguardRepository.setKeyguardShowing(false)
657             kosmos.setTransition(
658                 sceneTransition = Idle(Scenes.Gone),
659                 stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE),
660             )
661 
662             // WHEN: the notification is updated by the Server
663             collectionListener.onEntryUpdated(fakeEntry, UpdateSource.SystemServer)
664             testScheduler.runCurrent()
665 
666             // WHEN: Keyguard is shown again
667             keyguardRepository.setKeyguardShowing(true)
668             kosmos.setTransition(
669                 sceneTransition = Idle(Scenes.Lockscreen),
670                 stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD),
671             )
672 
673             // THEN: The notification is still recognized as "seen" and is filtered out.
674             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
675         }
676     }
677 
678     @Test
seenNotificationOnKeyguardMarkedAsSeenIfUpdatedBySystemUinull679     fun seenNotificationOnKeyguardMarkedAsSeenIfUpdatedBySystemUi() {
680         // GIVEN: Keyguard is showing, not dozing, unseen notification is present
681         keyguardRepository.setKeyguardShowing(true)
682         keyguardRepository.setIsDozing(false)
683         runKeyguardCoordinatorTest {
684             val fakeEntry = NotificationEntryBuilder().build()
685             collectionListener.onEntryAdded(fakeEntry)
686             keyguardTransitionRepository.sendTransitionSteps(
687                 from = KeyguardState.AOD,
688                 to = KeyguardState.LOCKSCREEN,
689                 this.testScheduler,
690             )
691             testScheduler.runCurrent()
692 
693             // WHEN: five seconds have passed
694             testScheduler.advanceTimeBy(5.seconds)
695             testScheduler.runCurrent()
696 
697             // WHEN: Keyguard is no longer showing
698             keyguardRepository.setKeyguardShowing(false)
699             kosmos.setTransition(
700                 sceneTransition = Idle(Scenes.Gone),
701                 stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE),
702             )
703 
704             // WHEN: the notification is updated by the SystemUi
705             collectionListener.onEntryUpdated(fakeEntry, UpdateSource.SystemUi)
706             testScheduler.runCurrent()
707 
708             // WHEN: Keyguard is shown again
709             keyguardRepository.setKeyguardShowing(true)
710             kosmos.setTransition(
711                 sceneTransition = Idle(Scenes.Lockscreen),
712                 stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD),
713             )
714 
715             // THEN: The notification is still recognized as "seen" and is filtered out.
716             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
717         }
718     }
719 
720     @Test
seenNotificationOnKeyguardMarkedAsUnseenIfUpdatedByAppnull721     fun seenNotificationOnKeyguardMarkedAsUnseenIfUpdatedByApp() {
722         // GIVEN: Keyguard is showing, not dozing, unseen notification is present
723         keyguardRepository.setKeyguardShowing(true)
724         keyguardRepository.setIsDozing(false)
725         runKeyguardCoordinatorTest {
726             val fakeEntry = NotificationEntryBuilder().build()
727             collectionListener.onEntryAdded(fakeEntry)
728             keyguardTransitionRepository.sendTransitionSteps(
729                 from = KeyguardState.AOD,
730                 to = KeyguardState.LOCKSCREEN,
731                 this.testScheduler,
732             )
733             testScheduler.runCurrent()
734 
735             // WHEN: five seconds have passed
736             testScheduler.advanceTimeBy(5.seconds)
737             testScheduler.runCurrent()
738 
739             // WHEN: Keyguard is no longer showing
740             keyguardRepository.setKeyguardShowing(false)
741             kosmos.setTransition(
742                 sceneTransition = Idle(Scenes.Gone),
743                 stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE),
744             )
745 
746             // WHEN: the notification is updated by the App
747             collectionListener.onEntryUpdated(fakeEntry, UpdateSource.App)
748             testScheduler.runCurrent()
749 
750             // WHEN: Keyguard is shown again
751             keyguardRepository.setKeyguardShowing(true)
752             kosmos.setTransition(
753                 sceneTransition = Idle(Scenes.Lockscreen),
754                 stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD),
755             )
756 
757             // THEN: The notification is now recognized as "unseen" and is not filtered out.
758             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
759         }
760     }
761 
runKeyguardCoordinatorTestnull762     private fun runKeyguardCoordinatorTest(
763         testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
764     ) {
765         val keyguardCoordinator =
766             OriginalUnseenKeyguardCoordinator(
767                 dumpManager = kosmos.dumpManager,
768                 headsUpManager = kosmos.mockHeadsUpManager,
769                 keyguardRepository = kosmos.keyguardRepository,
770                 keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
771                 logger = KeyguardCoordinatorLogger(logcatLogBuffer()),
772                 scope = kosmos.testScope.backgroundScope,
773                 seenNotificationsInteractor = kosmos.seenNotificationsInteractor,
774                 statusBarStateController = kosmos.statusBarStateController,
775                 sceneInteractor = kosmos.sceneInteractor,
776             )
777         keyguardCoordinator.attach(notifPipeline)
778         kosmos.testScope.runTest {
779             KeyguardCoordinatorTestScope(
780                     keyguardCoordinator,
781                     kosmos.testScope,
782                     kosmos.seenNotificationsInteractor,
783                     kosmos.fakeSettings,
784                 )
785                 .testBlock()
786         }
787     }
788 
789     private inner class KeyguardCoordinatorTestScope(
790         private val keyguardCoordinator: OriginalUnseenKeyguardCoordinator,
791         private val scope: TestScope,
792         val seenNotificationsInteractor: SeenNotificationsInteractor,
793         private val fakeSettings: FakeSettings,
<lambda>null794     ) : CoroutineScope by scope {
795         val testScheduler: TestCoroutineScheduler
796             get() = scope.testScheduler
797 
798         val unseenFilter: NotifFilter
799             get() = keyguardCoordinator.unseenNotifFilter
800 
801         val collectionListener: NotifCollectionListener =
802             argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue
803 
804         val onHeadsUpChangedListener: OnHeadsUpChangedListener
805             get() =
806                 argumentCaptor { verify(kosmos.mockHeadsUpManager).addListener(capture()) }
807                     .lastValue
808     }
809 
810     companion object {
811         @JvmStatic
812         @Parameters(name = "{0}")
getParamsnull813         fun getParams(): List<FlagsParameterization> {
814             return FlagsParameterization.allCombinationsOf().andSceneContainer()
815         }
816     }
817 }
818