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