1 /* 2 * Copyright (C) 2021 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 17 package com.android.systemui.statusbar.phone.ongoingcall 18 19 import android.app.ActivityManager 20 import android.app.IActivityManager 21 import android.app.IUidObserver 22 import android.app.Notification 23 import android.app.PendingIntent 24 import android.app.Person 25 import android.service.notification.NotificationListenerService.REASON_USER_STOPPED 26 import android.testing.AndroidTestingRunner 27 import android.testing.TestableLooper 28 import android.view.LayoutInflater 29 import android.view.View 30 import android.widget.LinearLayout 31 import androidx.test.filters.SmallTest 32 import com.android.internal.logging.testing.UiEventLoggerFake 33 import com.android.systemui.R 34 import com.android.systemui.SysuiTestCase 35 import com.android.systemui.dump.DumpManager 36 import com.android.systemui.plugins.ActivityStarter 37 import com.android.systemui.plugins.statusbar.StatusBarStateController 38 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler 39 import com.android.systemui.statusbar.notification.collection.NotificationEntry 40 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder 41 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection 42 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener 43 import com.android.systemui.statusbar.window.StatusBarWindowController 44 import com.android.systemui.util.concurrency.FakeExecutor 45 import com.android.systemui.util.mockito.any 46 import com.android.systemui.util.time.FakeSystemClock 47 import com.google.common.truth.Truth.assertThat 48 import org.junit.After 49 import org.junit.Before 50 import org.junit.Test 51 import org.junit.runner.RunWith 52 import org.mockito.ArgumentCaptor 53 import org.mockito.ArgumentMatchers.anyBoolean 54 import org.mockito.ArgumentMatchers.anyString 55 import org.mockito.ArgumentMatchers.nullable 56 import org.mockito.Mock 57 import org.mockito.Mockito.`when` 58 import org.mockito.Mockito.eq 59 import org.mockito.Mockito.mock 60 import org.mockito.Mockito.never 61 import org.mockito.Mockito.reset 62 import org.mockito.Mockito.times 63 import org.mockito.Mockito.verify 64 import org.mockito.MockitoAnnotations 65 import java.util.* 66 67 private const val CALL_UID = 900 68 69 // A process state that represents the process being visible to the user. 70 private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP 71 72 // A process state that represents the process being invisible to the user. 73 private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE 74 75 @SmallTest 76 @RunWith(AndroidTestingRunner::class) 77 @TestableLooper.RunWithLooper 78 class OngoingCallControllerTest : SysuiTestCase() { 79 80 private val clock = FakeSystemClock() 81 private val mainExecutor = FakeExecutor(clock) 82 private val uiEventLoggerFake = UiEventLoggerFake() 83 84 private lateinit var controller: OngoingCallController 85 private lateinit var notifCollectionListener: NotifCollectionListener 86 87 @Mock private lateinit var mockOngoingCallFlags: OngoingCallFlags 88 @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler: 89 SwipeStatusBarAwayGestureHandler 90 @Mock private lateinit var mockOngoingCallListener: OngoingCallListener 91 @Mock private lateinit var mockActivityStarter: ActivityStarter 92 @Mock private lateinit var mockIActivityManager: IActivityManager 93 @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController 94 @Mock private lateinit var mockStatusBarStateController: StatusBarStateController 95 96 private lateinit var chipView: View 97 98 @Before 99 fun setUp() { 100 allowTestableLooperAsMainThread() 101 TestableLooper.get(this).runWithLooper { 102 chipView = LayoutInflater.from(mContext).inflate(R.layout.ongoing_call_chip, null) 103 } 104 105 MockitoAnnotations.initMocks(this) 106 `when`(mockOngoingCallFlags.isStatusBarChipEnabled()).thenReturn(true) 107 val notificationCollection = mock(CommonNotifCollection::class.java) 108 109 controller = OngoingCallController( 110 context, 111 notificationCollection, 112 mockOngoingCallFlags, 113 clock, 114 mockActivityStarter, 115 mainExecutor, 116 mockIActivityManager, 117 OngoingCallLogger(uiEventLoggerFake), 118 DumpManager(), 119 Optional.of(mockStatusBarWindowController), 120 Optional.of(mockSwipeStatusBarAwayGestureHandler), 121 mockStatusBarStateController, 122 ) 123 controller.init() 124 controller.addCallback(mockOngoingCallListener) 125 controller.setChipView(chipView) 126 127 val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) 128 verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) 129 notifCollectionListener = collectionListenerCaptor.value!! 130 131 `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) 132 .thenReturn(PROC_STATE_INVISIBLE) 133 } 134 135 @After 136 fun tearDown() { 137 controller.tearDownChipView() 138 } 139 140 @Test 141 fun onEntryUpdated_isOngoingCallNotif_listenerNotified() { 142 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 143 144 verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) 145 } 146 147 @Test 148 fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() { 149 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 150 151 verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true) 152 } 153 154 @Test 155 fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() { 156 notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) 157 158 verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) 159 } 160 161 @Test 162 fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_listenerNotifiedTwice() { 163 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 164 notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) 165 166 verify(mockOngoingCallListener, times(2)) 167 .onOngoingCallStateChanged(anyBoolean()) 168 } 169 170 /** Regression test for b/191472854. */ 171 @Test 172 fun onEntryUpdated_notifHasNullContentIntent_noCrash() { 173 notifCollectionListener.onEntryUpdated( 174 createCallNotifEntry(ongoingCallStyle, nullContentIntent = true)) 175 } 176 177 /** Regression test for b/192379214. */ 178 @Test 179 fun onEntryUpdated_notificationWhenIsZero_timeHidden() { 180 val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) 181 notification.modifyNotification(context).setWhen(0) 182 183 notifCollectionListener.onEntryUpdated(notification.build()) 184 chipView.measure( 185 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), 186 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) 187 ) 188 189 assertThat(chipView.findViewById<View>(R.id.ongoing_call_chip_time)?.measuredWidth) 190 .isEqualTo(0) 191 } 192 193 @Test 194 fun onEntryUpdated_notificationWhenIsValid_timeShown() { 195 val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) 196 notification.modifyNotification(context).setWhen(clock.currentTimeMillis()) 197 198 notifCollectionListener.onEntryUpdated(notification.build()) 199 chipView.measure( 200 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), 201 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) 202 ) 203 204 assertThat(chipView.findViewById<View>(R.id.ongoing_call_chip_time)?.measuredWidth) 205 .isGreaterThan(0) 206 } 207 208 /** Regression test for b/194731244. */ 209 @Test 210 fun onEntryUpdated_calledManyTimes_uidObserverOnlyRegisteredOnce() { 211 for (i in 0 until 4) { 212 // Re-create the notification each time so that it's considered a different object and 213 // will re-trigger the whole flow. 214 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 215 } 216 217 verify(mockIActivityManager, times(1)) 218 .registerUidObserver(any(), any(), any(), any()) 219 } 220 221 /** Regression test for b/216248574. */ 222 @Test 223 fun entryUpdated_getUidProcessStateThrowsException_noCrash() { 224 `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) 225 .thenThrow(SecurityException()) 226 227 // No assert required, just check no crash 228 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 229 } 230 231 /** Regression test for b/216248574. */ 232 @Test 233 fun entryUpdated_registerUidObserverThrowsException_noCrash() { 234 `when`(mockIActivityManager.registerUidObserver( 235 any(), any(), any(), nullable(String::class.java) 236 )).thenThrow(SecurityException()) 237 238 // No assert required, just check no crash 239 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 240 } 241 242 /** Regression test for b/216248574. */ 243 @Test 244 fun entryUpdated_packageNameProvidedToActivityManager() { 245 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 246 247 val packageNameCaptor = ArgumentCaptor.forClass(String::class.java) 248 verify(mockIActivityManager).registerUidObserver( 249 any(), any(), any(), packageNameCaptor.capture() 250 ) 251 assertThat(packageNameCaptor.value).isNotNull() 252 } 253 254 /** 255 * If a call notification is never added before #onEntryRemoved is called, then the listener 256 * should never be notified. 257 */ 258 @Test 259 fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() { 260 notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) 261 262 verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) 263 } 264 265 @Test 266 fun onEntryRemoved_callNotifAddedThenRemoved_listenerNotified() { 267 val ongoingCallNotifEntry = createOngoingCallNotifEntry() 268 notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) 269 reset(mockOngoingCallListener) 270 271 notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) 272 273 verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) 274 } 275 276 @Test 277 fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() { 278 val ongoingCallNotifEntry = createOngoingCallNotifEntry() 279 notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) 280 281 notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) 282 283 verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false) 284 } 285 286 /** Regression test for b/188491504. */ 287 @Test 288 fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() { 289 val ongoingCallNotifEntry = createOngoingCallNotifEntry() 290 notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) 291 reset(mockOngoingCallListener) 292 293 // Create another notification based on the ongoing call one, but remove the features that 294 // made it a call notification. 295 val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry) 296 removedEntryBuilder.modifyNotification(context).style = null 297 298 notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) 299 300 verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) 301 } 302 303 @Test 304 fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() { 305 notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry()) 306 reset(mockOngoingCallListener) 307 308 notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) 309 310 verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) 311 } 312 313 @Test 314 fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() { 315 assertThat(controller.hasOngoingCall()).isFalse() 316 } 317 318 @Test 319 fun hasOngoingCall_unrelatedNotifSent_returnsFalse() { 320 notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) 321 322 assertThat(controller.hasOngoingCall()).isFalse() 323 } 324 325 @Test 326 fun hasOngoingCall_screeningCallNotifSent_returnsFalse() { 327 notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) 328 329 assertThat(controller.hasOngoingCall()).isFalse() 330 } 331 332 @Test 333 fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() { 334 `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) 335 .thenReturn(PROC_STATE_INVISIBLE) 336 337 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 338 339 assertThat(controller.hasOngoingCall()).isTrue() 340 } 341 342 @Test 343 fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() { 344 `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) 345 .thenReturn(PROC_STATE_VISIBLE) 346 347 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 348 349 assertThat(controller.hasOngoingCall()).isFalse() 350 } 351 352 @Test 353 fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { 354 val invalidChipView = LinearLayout(context) 355 controller.setChipView(invalidChipView) 356 357 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 358 359 assertThat(controller.hasOngoingCall()).isFalse() 360 } 361 362 @Test 363 fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() { 364 val ongoingCallNotifEntry = createOngoingCallNotifEntry() 365 366 notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry) 367 notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) 368 369 assertThat(controller.hasOngoingCall()).isFalse() 370 } 371 372 @Test 373 fun hasOngoingCall_ongoingCallNotifSentThenScreeningCallNotifSent_returnsFalse() { 374 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 375 notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) 376 377 assertThat(controller.hasOngoingCall()).isFalse() 378 } 379 380 @Test 381 fun hasOngoingCall_ongoingCallNotifSentThenUnrelatedNotifSent_returnsTrue() { 382 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 383 notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) 384 385 assertThat(controller.hasOngoingCall()).isTrue() 386 } 387 388 /** 389 * This test fakes a theme change during an ongoing call. 390 * 391 * When a theme change happens, [CollapsedStatusBarFragment] and its views get re-created, so 392 * [OngoingCallController.setChipView] gets called with a new view. If there's an active ongoing 393 * call when the theme changes, the new view needs to be updated with the call information. 394 */ 395 @Test 396 fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() { 397 // Start an ongoing call. 398 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 399 reset(mockOngoingCallListener) 400 401 lateinit var newChipView: View 402 TestableLooper.get(this).runWithLooper { 403 newChipView = LayoutInflater.from(mContext).inflate(R.layout.ongoing_call_chip, null) 404 } 405 406 // Change the chip view associated with the controller. 407 controller.setChipView(newChipView) 408 409 verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) 410 } 411 412 @Test 413 fun callProcessChangesToVisible_listenerNotified() { 414 // Start the call while the process is invisible. 415 `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) 416 .thenReturn(PROC_STATE_INVISIBLE) 417 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 418 reset(mockOngoingCallListener) 419 420 val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) 421 verify(mockIActivityManager).registerUidObserver( 422 captor.capture(), any(), any(), nullable(String::class.java)) 423 val uidObserver = captor.value 424 425 // Update the process to visible. 426 uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0) 427 mainExecutor.advanceClockToLast() 428 mainExecutor.runAllReady() 429 430 verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) 431 } 432 433 @Test 434 fun callProcessChangesToInvisible_listenerNotified() { 435 // Start the call while the process is visible. 436 `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) 437 .thenReturn(PROC_STATE_VISIBLE) 438 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 439 reset(mockOngoingCallListener) 440 441 val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) 442 verify(mockIActivityManager).registerUidObserver( 443 captor.capture(), any(), any(), nullable(String::class.java)) 444 val uidObserver = captor.value 445 446 // Update the process to invisible. 447 uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0) 448 mainExecutor.advanceClockToLast() 449 mainExecutor.runAllReady() 450 451 verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) 452 } 453 454 @Test 455 fun chipClicked_clickEventLogged() { 456 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 457 458 chipView.performClick() 459 460 assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) 461 assertThat(uiEventLoggerFake.eventId(0)) 462 .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_CLICKED.id) 463 } 464 465 /** Regression test for b/212467440. */ 466 @Test 467 fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() { 468 val notifEntry = createOngoingCallNotifEntry() 469 val pendingIntent = notifEntry.sbn.notification.contentIntent 470 notifCollectionListener.onEntryUpdated(notifEntry) 471 472 chipView.performClick() 473 474 // Ensure that the sysui didn't modify the notification's intent -- see b/212467440. 475 verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any()) 476 } 477 478 @Test 479 fun notifyChipVisibilityChanged_visibleEventLogged() { 480 controller.notifyChipVisibilityChanged(true) 481 482 assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) 483 assertThat(uiEventLoggerFake.eventId(0)) 484 .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_VISIBLE.id) 485 } 486 // Other tests for notifyChipVisibilityChanged are in [OngoingCallLogger], since 487 // [OngoingCallController.notifyChipVisibilityChanged] just delegates to that class. 488 489 @Test 490 fun callNotificationAdded_chipIsClickable() { 491 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 492 493 assertThat(chipView.hasOnClickListeners()).isTrue() 494 } 495 496 @Test 497 fun fullscreenIsTrue_thenCallNotificationAdded_chipNotClickable() { 498 `when`(mockOngoingCallFlags.isInImmersiveChipTapEnabled()).thenReturn(false) 499 500 getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true) 501 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 502 503 assertThat(chipView.hasOnClickListeners()).isFalse() 504 } 505 506 @Test 507 fun callNotificationAdded_thenFullscreenIsTrue_chipNotClickable() { 508 `when`(mockOngoingCallFlags.isInImmersiveChipTapEnabled()).thenReturn(false) 509 510 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 511 getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true) 512 513 assertThat(chipView.hasOnClickListeners()).isFalse() 514 } 515 516 @Test 517 fun fullscreenChangesToFalse_chipClickable() { 518 `when`(mockOngoingCallFlags.isInImmersiveChipTapEnabled()).thenReturn(false) 519 520 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 521 // First, update to true 522 getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true) 523 // Then, update to false 524 getStateListener().onFullscreenStateChanged(/* isFullscreen= */ false) 525 526 assertThat(chipView.hasOnClickListeners()).isTrue() 527 } 528 529 @Test 530 fun fullscreenIsTrue_butChipClickInImmersiveEnabled_chipClickable() { 531 `when`(mockOngoingCallFlags.isInImmersiveChipTapEnabled()).thenReturn(true) 532 533 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 534 getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true) 535 536 assertThat(chipView.hasOnClickListeners()).isTrue() 537 } 538 539 // Swipe gesture tests 540 541 @Test 542 fun callStartedInImmersiveMode_swipeGestureCallbackAdded() { 543 getStateListener().onFullscreenStateChanged(true) 544 545 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 546 547 verify(mockSwipeStatusBarAwayGestureHandler) 548 .addOnGestureDetectedCallback(anyString(), any()) 549 } 550 551 @Test 552 fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() { 553 getStateListener().onFullscreenStateChanged(false) 554 555 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 556 557 verify(mockSwipeStatusBarAwayGestureHandler, never()) 558 .addOnGestureDetectedCallback(anyString(), any()) 559 } 560 561 @Test 562 fun transitionToImmersiveMode_swipeGestureCallbackAdded() { 563 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 564 565 getStateListener().onFullscreenStateChanged(true) 566 567 verify(mockSwipeStatusBarAwayGestureHandler) 568 .addOnGestureDetectedCallback(anyString(), any()) 569 } 570 571 @Test 572 fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() { 573 getStateListener().onFullscreenStateChanged(true) 574 notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) 575 reset(mockSwipeStatusBarAwayGestureHandler) 576 577 getStateListener().onFullscreenStateChanged(false) 578 579 verify(mockSwipeStatusBarAwayGestureHandler) 580 .removeOnGestureDetectedCallback(anyString()) 581 } 582 583 @Test 584 fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() { 585 getStateListener().onFullscreenStateChanged(true) 586 val ongoingCallNotifEntry = createOngoingCallNotifEntry() 587 notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) 588 reset(mockSwipeStatusBarAwayGestureHandler) 589 590 notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) 591 592 verify(mockSwipeStatusBarAwayGestureHandler) 593 .removeOnGestureDetectedCallback(anyString()) 594 } 595 596 // TODO(b/195839150): Add test 597 // swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's 598 // difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s 599 // callbacks in test. 600 601 // END swipe gesture tests 602 603 private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle) 604 605 private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle) 606 607 private fun createCallNotifEntry( 608 callStyle: Notification.CallStyle, 609 nullContentIntent: Boolean = false 610 ): NotificationEntry { 611 val notificationEntryBuilder = NotificationEntryBuilder() 612 notificationEntryBuilder.modifyNotification(context).style = callStyle 613 notificationEntryBuilder.setUid(CALL_UID) 614 615 if (nullContentIntent) { 616 notificationEntryBuilder.modifyNotification(context).setContentIntent(null) 617 } else { 618 val contentIntent = mock(PendingIntent::class.java) 619 notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) 620 } 621 622 return notificationEntryBuilder.build() 623 } 624 625 private fun createNotCallNotifEntry() = NotificationEntryBuilder().build() 626 627 private fun getStateListener(): StatusBarStateController.StateListener { 628 val statusBarStateListenerCaptor = ArgumentCaptor.forClass( 629 StatusBarStateController.StateListener::class.java) 630 verify(mockStatusBarStateController).addCallback(statusBarStateListenerCaptor.capture()) 631 return statusBarStateListenerCaptor.value!! 632 } 633 } 634 635 private val person = Person.Builder().setName("name").build() 636 private val hangUpIntent = mock(PendingIntent::class.java) 637 638 private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent) 639 private val screeningCallStyle = Notification.CallStyle.forScreeningCall( 640 person, hangUpIntent, /* answerIntent= */ mock(PendingIntent::class.java))