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