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 17 package com.android.systemui.media.taptotransfer.sender 18 19 import android.app.StatusBarManager 20 import android.content.pm.ApplicationInfo 21 import android.content.pm.PackageManager 22 import android.graphics.drawable.Drawable 23 import android.media.MediaRoute2Info 24 import android.os.PowerManager 25 import android.os.VibrationAttributes 26 import android.os.VibrationEffect 27 import android.testing.AndroidTestingRunner 28 import android.testing.TestableLooper 29 import android.view.View 30 import android.view.ViewGroup 31 import android.view.WindowManager 32 import android.view.accessibility.AccessibilityManager 33 import android.widget.ImageView 34 import android.widget.TextView 35 import androidx.test.filters.SmallTest 36 import com.android.internal.logging.testing.UiEventLoggerFake 37 import com.android.internal.statusbar.IUndoMediaTransferCallback 38 import com.android.systemui.R 39 import com.android.systemui.SysuiTestCase 40 import com.android.systemui.classifier.FalsingCollector 41 import com.android.systemui.common.shared.model.Text.Companion.loadText 42 import com.android.systemui.dump.DumpManager 43 import com.android.systemui.flags.FakeFeatureFlags 44 import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION 45 import com.android.systemui.media.taptotransfer.MediaTttFlags 46 import com.android.systemui.plugins.FalsingManager 47 import com.android.systemui.statusbar.CommandQueue 48 import com.android.systemui.statusbar.VibratorHelper 49 import com.android.systemui.statusbar.policy.ConfigurationController 50 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController 51 import com.android.systemui.temporarydisplay.TemporaryViewUiEventLogger 52 import com.android.systemui.temporarydisplay.chipbar.ChipbarAnimator 53 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator 54 import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger 55 import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler 56 import com.android.systemui.util.concurrency.FakeExecutor 57 import com.android.systemui.util.mockito.any 58 import com.android.systemui.util.mockito.argumentCaptor 59 import com.android.systemui.util.mockito.capture 60 import com.android.systemui.util.mockito.eq 61 import com.android.systemui.util.mockito.mock 62 import com.android.systemui.util.time.FakeSystemClock 63 import com.android.systemui.util.view.ViewUtil 64 import com.android.systemui.util.wakelock.WakeLockFake 65 import com.google.common.truth.Truth.assertThat 66 import org.junit.Before 67 import org.junit.Test 68 import org.junit.runner.RunWith 69 import org.mockito.ArgumentCaptor 70 import org.mockito.Mock 71 import org.mockito.Mockito.atLeast 72 import org.mockito.Mockito.never 73 import org.mockito.Mockito.reset 74 import org.mockito.Mockito.verify 75 import org.mockito.Mockito.`when` as whenever 76 import org.mockito.MockitoAnnotations 77 78 @SmallTest 79 @RunWith(AndroidTestingRunner::class) 80 @TestableLooper.RunWithLooper 81 class MediaTttSenderCoordinatorTest : SysuiTestCase() { 82 83 // Note: This tests are a bit like integration tests because they use a real instance of 84 // [ChipbarCoordinator] and verify that the coordinator displays the correct view, based on 85 // the inputs from [MediaTttSenderCoordinator]. 86 87 private lateinit var underTest: MediaTttSenderCoordinator 88 89 @Mock private lateinit var accessibilityManager: AccessibilityManager 90 @Mock private lateinit var applicationInfo: ApplicationInfo 91 @Mock private lateinit var commandQueue: CommandQueue 92 @Mock private lateinit var configurationController: ConfigurationController 93 @Mock private lateinit var dumpManager: DumpManager 94 @Mock private lateinit var falsingManager: FalsingManager 95 @Mock private lateinit var falsingCollector: FalsingCollector 96 @Mock private lateinit var chipbarLogger: ChipbarLogger 97 @Mock private lateinit var logger: MediaTttSenderLogger 98 @Mock private lateinit var mediaTttFlags: MediaTttFlags 99 @Mock private lateinit var packageManager: PackageManager 100 @Mock private lateinit var powerManager: PowerManager 101 @Mock private lateinit var viewUtil: ViewUtil 102 @Mock private lateinit var windowManager: WindowManager 103 @Mock private lateinit var vibratorHelper: VibratorHelper 104 @Mock private lateinit var swipeHandler: SwipeChipbarAwayGestureHandler 105 private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder 106 private lateinit var fakeWakeLock: WakeLockFake 107 private lateinit var chipbarCoordinator: ChipbarCoordinator 108 private lateinit var commandQueueCallback: CommandQueue.Callbacks 109 private lateinit var fakeAppIconDrawable: Drawable 110 private lateinit var fakeClock: FakeSystemClock 111 private lateinit var fakeExecutor: FakeExecutor 112 private lateinit var uiEventLoggerFake: UiEventLoggerFake 113 private lateinit var uiEventLogger: MediaTttSenderUiEventLogger 114 private lateinit var tempViewUiEventLogger: TemporaryViewUiEventLogger 115 private val defaultTimeout = context.resources.getInteger(R.integer.heads_up_notification_decay) 116 private val featureFlags = FakeFeatureFlags() 117 118 @Before 119 fun setUp() { 120 MockitoAnnotations.initMocks(this) 121 whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true) 122 whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) 123 124 fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! 125 whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) 126 whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) 127 whenever( 128 packageManager.getApplicationInfo( 129 eq(PACKAGE_NAME), 130 any<PackageManager.ApplicationInfoFlags>() 131 ) 132 ) 133 .thenReturn(applicationInfo) 134 context.setMockPackageManager(packageManager) 135 136 fakeClock = FakeSystemClock() 137 fakeExecutor = FakeExecutor(fakeClock) 138 139 fakeWakeLock = WakeLockFake() 140 fakeWakeLockBuilder = WakeLockFake.Builder(context) 141 fakeWakeLockBuilder.setWakeLock(fakeWakeLock) 142 143 uiEventLoggerFake = UiEventLoggerFake() 144 uiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) 145 tempViewUiEventLogger = TemporaryViewUiEventLogger(uiEventLoggerFake) 146 147 chipbarCoordinator = 148 ChipbarCoordinator( 149 context, 150 chipbarLogger, 151 windowManager, 152 fakeExecutor, 153 accessibilityManager, 154 configurationController, 155 dumpManager, 156 powerManager, 157 ChipbarAnimator(), 158 falsingManager, 159 falsingCollector, 160 swipeHandler, 161 viewUtil, 162 vibratorHelper, 163 fakeWakeLockBuilder, 164 fakeClock, 165 tempViewUiEventLogger, 166 featureFlags 167 ) 168 featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) 169 chipbarCoordinator.start() 170 171 underTest = 172 MediaTttSenderCoordinator( 173 chipbarCoordinator, 174 commandQueue, 175 context, 176 dumpManager, 177 logger, 178 mediaTttFlags, 179 uiEventLogger, 180 ) 181 underTest.start() 182 183 setCommandQueueCallback() 184 } 185 186 @Test 187 fun commandQueueCallback_flagOff_noCallbackAdded() { 188 reset(commandQueue) 189 whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(false) 190 underTest = 191 MediaTttSenderCoordinator( 192 chipbarCoordinator, 193 commandQueue, 194 context, 195 dumpManager, 196 logger, 197 mediaTttFlags, 198 uiEventLogger, 199 ) 200 underTest.start() 201 202 verify(commandQueue, never()).addCallback(any()) 203 } 204 205 @Test 206 fun commandQueueCallback_almostCloseToStartCast_triggersCorrectChip() { 207 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 208 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 209 routeInfo, 210 null 211 ) 212 213 val chipbarView = getChipbarView() 214 assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) 215 assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) 216 assertThat(chipbarView.getChipText()) 217 .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) 218 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) 219 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 220 assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) 221 assertThat(uiEventLoggerFake.eventId(0)) 222 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id) 223 verify(vibratorHelper) 224 .vibrate( 225 any(), 226 any(), 227 any<VibrationEffect>(), 228 any(), 229 any<VibrationAttributes>(), 230 ) 231 } 232 233 @Test 234 fun commandQueueCallback_almostCloseToStartCast_deviceNameBlank_showsDefaultDeviceName() { 235 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 236 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 237 routeInfoWithBlankDeviceName, 238 null, 239 ) 240 241 val chipbarView = getChipbarView() 242 assertThat(chipbarView.getChipText()) 243 .contains(context.getString(R.string.media_ttt_default_device_type)) 244 assertThat(chipbarView.getChipText()) 245 .isNotEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) 246 } 247 248 @Test 249 fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() { 250 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 251 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 252 routeInfo, 253 null 254 ) 255 256 val chipbarView = getChipbarView() 257 assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) 258 assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) 259 assertThat(chipbarView.getChipText()) 260 .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText()) 261 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) 262 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 263 assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) 264 assertThat(uiEventLoggerFake.eventId(0)) 265 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id) 266 verify(vibratorHelper) 267 .vibrate( 268 any(), 269 any(), 270 any<VibrationEffect>(), 271 any(), 272 any<VibrationAttributes>(), 273 ) 274 } 275 276 @Test 277 fun commandQueueCallback_transferToReceiverTriggered_triggersCorrectChip() { 278 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 279 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, 280 routeInfo, 281 null 282 ) 283 284 val chipbarView = getChipbarView() 285 assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) 286 assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) 287 assertThat(chipbarView.getChipText()) 288 .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) 289 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) 290 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 291 assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) 292 assertThat(uiEventLoggerFake.eventId(0)) 293 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id) 294 verify(vibratorHelper) 295 .vibrate( 296 any(), 297 any(), 298 any<VibrationEffect>(), 299 any(), 300 any<VibrationAttributes>(), 301 ) 302 } 303 304 @Test 305 fun commandQueueCallback_transferToReceiverTriggered_deviceNameBlank_showsDefaultDeviceName() { 306 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 307 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, 308 routeInfoWithBlankDeviceName, 309 null, 310 ) 311 312 val chipbarView = getChipbarView() 313 assertThat(chipbarView.getChipText()) 314 .contains(context.getString(R.string.media_ttt_default_device_type)) 315 assertThat(chipbarView.getChipText()) 316 .isNotEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) 317 } 318 319 @Test 320 fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() { 321 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 322 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 323 routeInfo, 324 null 325 ) 326 327 val chipbarView = getChipbarView() 328 assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) 329 assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) 330 assertThat(chipbarView.getChipText()) 331 .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) 332 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) 333 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 334 assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) 335 assertThat(uiEventLoggerFake.eventId(0)) 336 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id) 337 verify(vibratorHelper) 338 .vibrate( 339 any(), 340 any(), 341 any<VibrationEffect>(), 342 any(), 343 any<VibrationAttributes>(), 344 ) 345 } 346 347 @Test 348 fun commandQueueCallback_transferToReceiverSucceeded_triggersCorrectChip() { 349 displayReceiverTriggered() 350 reset(vibratorHelper) 351 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 352 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 353 routeInfo, 354 null 355 ) 356 357 val chipbarView = getChipbarView() 358 assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) 359 assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) 360 assertThat(chipbarView.getChipText()) 361 .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) 362 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) 363 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 364 // Event index 2 since initially displaying the triggered chip would also log two events. 365 assertThat(uiEventLoggerFake.eventId(2)) 366 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id) 367 verify(vibratorHelper, never()) 368 .vibrate( 369 any(), 370 any(), 371 any<VibrationEffect>(), 372 any(), 373 any<VibrationAttributes>(), 374 ) 375 } 376 377 @Test 378 fun commandQueueCallback_transferToReceiverSucceeded_sameViewInstanceId() { 379 displayReceiverTriggered() 380 reset(vibratorHelper) 381 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 382 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 383 routeInfo, 384 null 385 ) 386 387 // Event index 2 since initially displaying the triggered chip would also log two events. 388 assertThat(uiEventLoggerFake.eventId(2)) 389 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id) 390 verify(vibratorHelper, never()).vibrate(any<VibrationEffect>()) 391 assertThat(uiEventLoggerFake.logs[0].instanceId) 392 .isEqualTo(uiEventLoggerFake.logs[2].instanceId) 393 } 394 395 @Test 396 fun transferToReceiverSucceeded_nullUndoCallback_noUndo() { 397 displayReceiverTriggered() 398 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 399 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 400 routeInfo, 401 /* undoCallback= */ null 402 ) 403 404 val chipbarView = getChipbarView() 405 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 406 } 407 408 @Test 409 fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() { 410 displayReceiverTriggered() 411 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 412 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 413 routeInfo, 414 /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { 415 override fun onUndoTriggered() {} 416 }, 417 ) 418 419 val chipbarView = getChipbarView() 420 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) 421 assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() 422 } 423 424 @Test 425 fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { 426 var undoCallbackCalled = false 427 displayReceiverTriggered() 428 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 429 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 430 routeInfo, 431 /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { 432 override fun onUndoTriggered() { 433 undoCallbackCalled = true 434 } 435 }, 436 ) 437 438 getChipbarView().getUndoButton().performClick() 439 440 // Event index 3 since initially displaying the triggered and succeeded chip would also log 441 // events. 442 assertThat(uiEventLoggerFake.eventId(3)) 443 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id) 444 assertThat(undoCallbackCalled).isTrue() 445 assertThat(getChipbarView().getChipText()) 446 .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) 447 } 448 449 @Test 450 fun commandQueueCallback_transferToThisDeviceSucceeded_triggersCorrectChip() { 451 displayThisDeviceTriggered() 452 reset(vibratorHelper) 453 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 454 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 455 routeInfo, 456 null 457 ) 458 459 val chipbarView = getChipbarView() 460 assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) 461 assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) 462 assertThat(chipbarView.getChipText()) 463 .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) 464 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) 465 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 466 // Event index 2 since initially displaying the triggered chip would also log two events. 467 assertThat(uiEventLoggerFake.eventId(2)) 468 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id) 469 verify(vibratorHelper, never()) 470 .vibrate( 471 any(), 472 any(), 473 any<VibrationEffect>(), 474 any(), 475 any<VibrationAttributes>(), 476 ) 477 } 478 479 @Test 480 fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() { 481 displayThisDeviceTriggered() 482 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 483 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 484 routeInfo, 485 /* undoCallback= */ null 486 ) 487 488 val chipbarView = getChipbarView() 489 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 490 } 491 492 @Test 493 fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() { 494 displayThisDeviceTriggered() 495 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 496 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 497 routeInfo, 498 /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { 499 override fun onUndoTriggered() {} 500 }, 501 ) 502 503 val chipbarView = getChipbarView() 504 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) 505 assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() 506 } 507 508 @Test 509 fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { 510 var undoCallbackCalled = false 511 displayThisDeviceTriggered() 512 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 513 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 514 routeInfo, 515 /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { 516 override fun onUndoTriggered() { 517 undoCallbackCalled = true 518 } 519 }, 520 ) 521 522 getChipbarView().getUndoButton().performClick() 523 524 // Event index 3 since initially displaying the triggered and succeeded chip would also log 525 // events. 526 assertThat(uiEventLoggerFake.eventId(3)) 527 .isEqualTo( 528 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id 529 ) 530 assertThat(undoCallbackCalled).isTrue() 531 assertThat(getChipbarView().getChipText()) 532 .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) 533 } 534 535 @Test 536 fun commandQueueCallback_transferToReceiverFailed_triggersCorrectChip() { 537 displayReceiverTriggered() 538 reset(vibratorHelper) 539 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 540 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, 541 routeInfo, 542 null 543 ) 544 545 val chipbarView = getChipbarView() 546 assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) 547 assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) 548 assertThat(chipbarView.getChipText()) 549 .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) 550 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) 551 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 552 assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) 553 // Event index 2 since initially displaying the triggered chip would also log two events. 554 assertThat(uiEventLoggerFake.eventId(2)) 555 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id) 556 verify(vibratorHelper) 557 .vibrate( 558 any(), 559 any(), 560 any<VibrationEffect>(), 561 any(), 562 any<VibrationAttributes>(), 563 ) 564 } 565 566 @Test 567 fun commandQueueCallback_transferToThisDeviceFailed_triggersCorrectChip() { 568 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 569 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 570 routeInfo, 571 null 572 ) 573 reset(vibratorHelper) 574 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 575 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED, 576 routeInfo, 577 null 578 ) 579 580 val chipbarView = getChipbarView() 581 assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) 582 assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) 583 assertThat(chipbarView.getChipText()) 584 .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) 585 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) 586 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) 587 assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) 588 // Event index 1 since initially displaying the triggered chip would also log an event. 589 assertThat(uiEventLoggerFake.eventId(2)) 590 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id) 591 verify(vibratorHelper) 592 .vibrate( 593 any(), 594 any(), 595 any<VibrationEffect>(), 596 any(), 597 any<VibrationAttributes>(), 598 ) 599 } 600 601 @Test 602 fun commandQueueCallback_farFromReceiver_noChipShown() { 603 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 604 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 605 routeInfo, 606 null 607 ) 608 609 verify(windowManager, never()).addView(any(), any()) 610 assertThat(uiEventLoggerFake.eventId(0)) 611 .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER.id) 612 } 613 614 @Test 615 fun commandQueueCallback_almostCloseThenFarFromReceiver_chipShownThenHidden() { 616 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 617 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 618 routeInfo, 619 null 620 ) 621 622 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 623 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 624 routeInfo, 625 null 626 ) 627 628 val viewCaptor = ArgumentCaptor.forClass(View::class.java) 629 verify(windowManager).addView(viewCaptor.capture(), any()) 630 verify(windowManager).removeView(viewCaptor.value) 631 verify(logger).logStateMapRemoval(eq(DEFAULT_ID), any()) 632 } 633 634 @Test 635 fun commandQueueCallback_almostCloseThenFarFromReceiver_wakeLockAcquiredThenReleased() { 636 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 637 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 638 routeInfo, 639 null 640 ) 641 642 assertThat(fakeWakeLock.isHeld).isTrue() 643 644 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 645 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 646 routeInfo, 647 null 648 ) 649 650 assertThat(fakeWakeLock.isHeld).isFalse() 651 } 652 653 @Test 654 fun commandQueueCallback_FarFromReceiver_wakeLockNeverReleased() { 655 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 656 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 657 routeInfo, 658 null 659 ) 660 661 assertThat(fakeWakeLock.isHeld).isFalse() 662 } 663 664 @Test 665 fun commandQueueCallback_invalidStateParam_noChipShown() { 666 commandQueueCallback.updateMediaTapToTransferSenderDisplay(100, routeInfo, null) 667 668 verify(windowManager, never()).addView(any(), any()) 669 } 670 671 @Test 672 fun commandQueueCallback_receiverTriggeredThenAlmostStart_invalidTransitionLogged() { 673 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 674 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, 675 routeInfo, 676 null 677 ) 678 verify(windowManager).addView(any(), any()) 679 reset(windowManager) 680 681 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 682 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 683 routeInfo, 684 null 685 ) 686 687 verify(logger).logInvalidStateTransitionError(any(), any()) 688 verify(windowManager, never()).addView(any(), any()) 689 } 690 691 @Test 692 fun commandQueueCallback_thisDeviceTriggeredThenAlmostEnd_invalidTransitionLogged() { 693 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 694 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 695 routeInfo, 696 null 697 ) 698 verify(windowManager).addView(any(), any()) 699 reset(windowManager) 700 701 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 702 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 703 routeInfo, 704 null 705 ) 706 707 verify(logger).logInvalidStateTransitionError(any(), any()) 708 verify(windowManager, never()).addView(any(), any()) 709 } 710 711 @Test 712 fun commandQueueCallback_receiverSucceededThenThisDeviceSucceeded_invalidTransitionLogged() { 713 displayReceiverTriggered() 714 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 715 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 716 routeInfo, 717 null 718 ) 719 reset(windowManager) 720 721 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 722 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 723 routeInfo, 724 null 725 ) 726 727 verify(logger).logInvalidStateTransitionError(any(), any()) 728 verify(windowManager, never()).addView(any(), any()) 729 } 730 731 @Test 732 fun commandQueueCallback_thisDeviceSucceededThenReceiverSucceeded_invalidTransitionLogged() { 733 displayThisDeviceTriggered() 734 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 735 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 736 routeInfo, 737 null 738 ) 739 reset(windowManager) 740 741 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 742 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 743 routeInfo, 744 null 745 ) 746 747 verify(logger).logInvalidStateTransitionError(any(), any()) 748 verify(windowManager, never()).addView(any(), any()) 749 } 750 751 @Test 752 fun commandQueueCallback_almostStartThenReceiverSucceeded_invalidTransitionLogged() { 753 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 754 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 755 routeInfo, 756 null 757 ) 758 verify(windowManager).addView(any(), any()) 759 reset(windowManager) 760 761 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 762 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 763 routeInfo, 764 null 765 ) 766 767 verify(logger).logInvalidStateTransitionError(any(), any()) 768 verify(windowManager, never()).addView(any(), any()) 769 } 770 771 @Test 772 fun commandQueueCallback_almostEndThenThisDeviceSucceeded_invalidTransitionLogged() { 773 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 774 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 775 routeInfo, 776 null 777 ) 778 verify(windowManager).addView(any(), any()) 779 reset(windowManager) 780 781 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 782 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 783 routeInfo, 784 null 785 ) 786 787 verify(logger).logInvalidStateTransitionError(any(), any()) 788 verify(windowManager, never()).addView(any(), any()) 789 } 790 791 @Test 792 fun commandQueueCallback_AlmostStartThenReceiverFailed_invalidTransitionLogged() { 793 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 794 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 795 routeInfo, 796 null 797 ) 798 verify(windowManager).addView(any(), any()) 799 reset(windowManager) 800 801 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 802 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, 803 routeInfo, 804 null 805 ) 806 807 verify(logger).logInvalidStateTransitionError(any(), any()) 808 verify(windowManager, never()).addView(any(), any()) 809 } 810 811 @Test 812 fun commandQueueCallback_almostEndThenThisDeviceFailed_invalidTransitionLogged() { 813 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 814 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 815 routeInfo, 816 null 817 ) 818 verify(windowManager).addView(any(), any()) 819 reset(windowManager) 820 821 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 822 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED, 823 routeInfo, 824 null 825 ) 826 827 verify(logger).logInvalidStateTransitionError(any(), any()) 828 verify(windowManager, never()).addView(any(), any()) 829 } 830 831 /** Regression test for b/266217596. */ 832 @Test 833 fun toReceiver_triggeredThenFar_thenSucceeded_updatesToSucceeded() { 834 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 835 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, 836 routeInfo, 837 null, 838 ) 839 840 // WHEN a FAR command comes in 841 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 842 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 843 routeInfo, 844 null, 845 ) 846 847 // THEN it is ignored and the chipbar is stilled displayed 848 val chipbarView = getChipbarView() 849 assertThat(chipbarView.getChipText()) 850 .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) 851 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) 852 verify(windowManager, never()).removeView(any()) 853 854 // WHEN a SUCCEEDED command comes in 855 val succeededRouteInfo = 856 MediaRoute2Info.Builder(DEFAULT_ID, "Tablet Succeeded") 857 .addFeature("feature") 858 .setClientPackageName(PACKAGE_NAME) 859 .build() 860 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 861 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 862 succeededRouteInfo, 863 /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { 864 override fun onUndoTriggered() {} 865 }, 866 ) 867 868 // THEN it is *not* marked as an invalid transition and the chipbar updates to the succeeded 869 // state. (The "invalid transition" would be FAR => SUCCEEDED.) 870 assertThat(chipbarView.getChipText()) 871 .isEqualTo( 872 ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText( 873 "Tablet Succeeded" 874 ) 875 ) 876 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) 877 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) 878 } 879 880 /** Regression test for b/266217596. */ 881 @Test 882 fun toThisDevice_triggeredThenFar_thenSucceeded_updatesToSucceeded() { 883 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 884 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 885 routeInfo, 886 null, 887 ) 888 889 // WHEN a FAR command comes in 890 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 891 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 892 routeInfo, 893 null, 894 ) 895 896 // THEN it is ignored and the chipbar is stilled displayed 897 val chipbarView = getChipbarView() 898 assertThat(chipbarView.getChipText()) 899 .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) 900 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) 901 verify(windowManager, never()).removeView(any()) 902 903 // WHEN a SUCCEEDED command comes in 904 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 905 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 906 routeInfo, 907 /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { 908 override fun onUndoTriggered() {} 909 }, 910 ) 911 912 // THEN it is *not* marked as an invalid transition and the chipbar updates to the succeeded 913 // state. (The "invalid transition" would be FAR => SUCCEEDED.) 914 assertThat(chipbarView.getChipText()) 915 .isEqualTo( 916 ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText( 917 "Tablet Succeeded" 918 ) 919 ) 920 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) 921 assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) 922 } 923 924 @Test 925 fun receivesNewStateFromCommandQueue_isLogged() { 926 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 927 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 928 routeInfo, 929 null 930 ) 931 932 verify(logger).logStateChange(any(), any(), any()) 933 } 934 935 @Test 936 fun transferToReceiverTriggeredThenFarFromReceiver_viewStillDisplayedButStillTimesOut() { 937 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 938 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, 939 routeInfo, 940 null 941 ) 942 943 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 944 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 945 routeInfo, 946 null 947 ) 948 fakeExecutor.runAllReady() 949 950 verify(windowManager, never()).removeView(any()) 951 verify(logger).logRemovalBypass(any(), any()) 952 953 fakeClock.advanceTime(TIMEOUT + 1L) 954 955 verify(windowManager).removeView(any()) 956 } 957 958 @Test 959 fun transferToThisDeviceTriggeredThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() { 960 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 961 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 962 routeInfo, 963 null 964 ) 965 966 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 967 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 968 routeInfo, 969 null 970 ) 971 fakeExecutor.runAllReady() 972 973 verify(windowManager, never()).removeView(any()) 974 verify(logger).logRemovalBypass(any(), any()) 975 976 fakeClock.advanceTime(TIMEOUT + 1L) 977 978 verify(windowManager).removeView(any()) 979 } 980 981 @Test 982 fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() { 983 displayReceiverTriggered() 984 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 985 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 986 routeInfo, 987 null 988 ) 989 990 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 991 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 992 routeInfo, 993 null 994 ) 995 fakeExecutor.runAllReady() 996 997 verify(windowManager, never()).removeView(any()) 998 verify(logger).logRemovalBypass(any(), any()) 999 1000 fakeClock.advanceTime(TIMEOUT + 1L) 1001 1002 verify(windowManager).removeView(any()) 1003 } 1004 1005 @Test 1006 fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() { 1007 displayThisDeviceTriggered() 1008 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1009 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 1010 routeInfo, 1011 null 1012 ) 1013 1014 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1015 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 1016 routeInfo, 1017 null 1018 ) 1019 fakeExecutor.runAllReady() 1020 1021 verify(windowManager, never()).removeView(any()) 1022 verify(logger).logRemovalBypass(any(), any()) 1023 1024 fakeClock.advanceTime(TIMEOUT + 1L) 1025 1026 verify(windowManager).removeView(any()) 1027 } 1028 1029 @Test 1030 fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { 1031 displayReceiverTriggered() 1032 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1033 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 1034 routeInfo, 1035 object : IUndoMediaTransferCallback.Stub() { 1036 override fun onUndoTriggered() {} 1037 }, 1038 ) 1039 val chipbarView = getChipbarView() 1040 assertThat(chipbarView.getChipText()) 1041 .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) 1042 1043 // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should 1044 // verify that the new state it triggers operates just like any other state. 1045 getChipbarView().getUndoButton().performClick() 1046 fakeExecutor.runAllReady() 1047 1048 // Verify that the click updated us to the triggered state 1049 assertThat(chipbarView.getChipText()) 1050 .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) 1051 1052 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1053 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 1054 routeInfo, 1055 null 1056 ) 1057 fakeExecutor.runAllReady() 1058 1059 // Verify that we didn't remove the chipbar because it's in the triggered state 1060 verify(windowManager, never()).removeView(any()) 1061 verify(logger).logRemovalBypass(any(), any()) 1062 1063 fakeClock.advanceTime(TIMEOUT + 1L) 1064 1065 // Verify we eventually remove the chipbar 1066 verify(windowManager).removeView(any()) 1067 } 1068 1069 @Test 1070 fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { 1071 displayThisDeviceTriggered() 1072 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1073 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 1074 routeInfo, 1075 object : IUndoMediaTransferCallback.Stub() { 1076 override fun onUndoTriggered() {} 1077 }, 1078 ) 1079 val chipbarView = getChipbarView() 1080 assertThat(chipbarView.getChipText()) 1081 .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) 1082 1083 // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should 1084 // verify that the new state it triggers operates just like any other state. 1085 getChipbarView().getUndoButton().performClick() 1086 fakeExecutor.runAllReady() 1087 1088 // Verify that the click updated us to the triggered state 1089 assertThat(chipbarView.getChipText()) 1090 .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) 1091 1092 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1093 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 1094 routeInfo, 1095 null 1096 ) 1097 fakeExecutor.runAllReady() 1098 1099 // Verify that we didn't remove the chipbar because it's in the triggered state 1100 verify(windowManager, never()).removeView(any()) 1101 verify(logger).logRemovalBypass(any(), any()) 1102 1103 fakeClock.advanceTime(TIMEOUT + 1L) 1104 1105 // Verify we eventually remove the chipbar 1106 verify(windowManager).removeView(any()) 1107 } 1108 1109 @Test 1110 fun newState_viewListenerRegistered() { 1111 val mockChipbarCoordinator = mock<ChipbarCoordinator>() 1112 whenever(mockChipbarCoordinator.tempViewUiEventLogger).thenReturn(tempViewUiEventLogger) 1113 underTest = 1114 MediaTttSenderCoordinator( 1115 mockChipbarCoordinator, 1116 commandQueue, 1117 context, 1118 dumpManager, 1119 logger, 1120 mediaTttFlags, 1121 uiEventLogger, 1122 ) 1123 underTest.start() 1124 // Re-set the command queue callback since we've created a new [MediaTttSenderCoordinator] 1125 // with a new callback. 1126 setCommandQueueCallback() 1127 1128 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1129 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 1130 routeInfo, 1131 null, 1132 ) 1133 1134 verify(mockChipbarCoordinator).registerListener(any()) 1135 } 1136 1137 @Test 1138 fun onInfoPermanentlyRemoved_viewListenerUnregistered() { 1139 val mockChipbarCoordinator = mock<ChipbarCoordinator>() 1140 whenever(mockChipbarCoordinator.tempViewUiEventLogger).thenReturn(tempViewUiEventLogger) 1141 underTest = 1142 MediaTttSenderCoordinator( 1143 mockChipbarCoordinator, 1144 commandQueue, 1145 context, 1146 dumpManager, 1147 logger, 1148 mediaTttFlags, 1149 uiEventLogger, 1150 ) 1151 underTest.start() 1152 setCommandQueueCallback() 1153 1154 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1155 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 1156 routeInfo, 1157 null, 1158 ) 1159 1160 val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>() 1161 verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor)) 1162 1163 // WHEN the listener is notified that the view has been removed 1164 listenerCaptor.value.onInfoPermanentlyRemoved(DEFAULT_ID, "reason") 1165 1166 // THEN the media coordinator unregisters the listener 1167 verify(logger).logStateMapRemoval(DEFAULT_ID, "reason") 1168 verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value) 1169 } 1170 1171 @Test 1172 fun onInfoPermanentlyRemoved_wrongId_viewListenerNotUnregistered() { 1173 val mockChipbarCoordinator = mock<ChipbarCoordinator>() 1174 whenever(mockChipbarCoordinator.tempViewUiEventLogger).thenReturn(tempViewUiEventLogger) 1175 underTest = 1176 MediaTttSenderCoordinator( 1177 mockChipbarCoordinator, 1178 commandQueue, 1179 context, 1180 dumpManager, 1181 logger, 1182 mediaTttFlags, 1183 uiEventLogger, 1184 ) 1185 underTest.start() 1186 setCommandQueueCallback() 1187 1188 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1189 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 1190 routeInfo, 1191 null, 1192 ) 1193 1194 val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>() 1195 verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor)) 1196 1197 // WHEN the listener is notified that a different view has been removed 1198 listenerCaptor.value.onInfoPermanentlyRemoved("differentViewId", "reason") 1199 1200 // THEN the media coordinator doesn't unregister the listener 1201 verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value) 1202 } 1203 1204 @Test 1205 fun farFromReceiverState_viewListenerUnregistered() { 1206 val mockChipbarCoordinator = mock<ChipbarCoordinator>() 1207 whenever(mockChipbarCoordinator.tempViewUiEventLogger).thenReturn(tempViewUiEventLogger) 1208 underTest = 1209 MediaTttSenderCoordinator( 1210 mockChipbarCoordinator, 1211 commandQueue, 1212 context, 1213 dumpManager, 1214 logger, 1215 mediaTttFlags, 1216 uiEventLogger, 1217 ) 1218 underTest.start() 1219 setCommandQueueCallback() 1220 1221 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1222 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 1223 routeInfo, 1224 null, 1225 ) 1226 1227 val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>() 1228 verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor)) 1229 1230 // WHEN we go to the FAR_FROM_RECEIVER state 1231 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1232 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 1233 routeInfo, 1234 null 1235 ) 1236 1237 // THEN the media coordinator unregisters the listener 1238 verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value) 1239 } 1240 1241 @Test 1242 fun statesWithDifferentIds_onInfoPermanentlyRemovedForOneId_viewListenerNotUnregistered() { 1243 val mockChipbarCoordinator = mock<ChipbarCoordinator>() 1244 whenever(mockChipbarCoordinator.tempViewUiEventLogger).thenReturn(tempViewUiEventLogger) 1245 underTest = 1246 MediaTttSenderCoordinator( 1247 mockChipbarCoordinator, 1248 commandQueue, 1249 context, 1250 dumpManager, 1251 logger, 1252 mediaTttFlags, 1253 uiEventLogger, 1254 ) 1255 underTest.start() 1256 setCommandQueueCallback() 1257 1258 // WHEN there are two different media transfers with different IDs 1259 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1260 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 1261 MediaRoute2Info.Builder("route1", OTHER_DEVICE_NAME) 1262 .addFeature("feature") 1263 .setClientPackageName(PACKAGE_NAME) 1264 .build(), 1265 null, 1266 ) 1267 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1268 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 1269 MediaRoute2Info.Builder("route2", OTHER_DEVICE_NAME) 1270 .addFeature("feature") 1271 .setClientPackageName(PACKAGE_NAME) 1272 .build(), 1273 null, 1274 ) 1275 1276 val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>() 1277 verify(mockChipbarCoordinator, atLeast(1)).registerListener(capture(listenerCaptor)) 1278 1279 // THEN one of them is removed 1280 listenerCaptor.value.onInfoPermanentlyRemoved("route1", "reason") 1281 1282 // THEN the media coordinator doesn't unregister the listener (since route2 is still active) 1283 verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value) 1284 verify(logger).logStateMapRemoval("route1", "reason") 1285 } 1286 1287 /** Regression test for b/266218672. */ 1288 @Test 1289 fun twoIdsDisplayed_oldIdIsFar_viewStillDisplayed() { 1290 // WHEN there are two different media transfers with different IDs 1291 val route1 = 1292 MediaRoute2Info.Builder("route1", OTHER_DEVICE_NAME) 1293 .addFeature("feature") 1294 .setClientPackageName(PACKAGE_NAME) 1295 .build() 1296 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1297 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 1298 route1, 1299 null, 1300 ) 1301 verify(windowManager).addView(any(), any()) 1302 reset(windowManager) 1303 1304 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1305 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 1306 MediaRoute2Info.Builder("route2", "Route 2 name") 1307 .addFeature("feature") 1308 .setClientPackageName(PACKAGE_NAME) 1309 .build(), 1310 null, 1311 ) 1312 val newView = getChipbarView() 1313 1314 // WHEN there's a FAR event for the earlier one 1315 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1316 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, 1317 route1, 1318 null, 1319 ) 1320 1321 // THEN it's ignored and the more recent one is still displayed 1322 assertThat(newView.getChipText()) 1323 .isEqualTo( 1324 ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText("Route 2 name") 1325 ) 1326 } 1327 1328 /** Regression test for b/266218672. */ 1329 @Test 1330 fun receiverSucceededThenTimedOut_internalStateResetAndCanDisplayAlmostCloseToEnd() { 1331 displayReceiverTriggered() 1332 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1333 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 1334 routeInfo, 1335 null, 1336 ) 1337 1338 fakeClock.advanceTime(TIMEOUT + 1L) 1339 verify(windowManager).removeView(any()) 1340 1341 reset(windowManager) 1342 1343 // WHEN we try to show ALMOST_CLOSE_TO_END 1344 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1345 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, 1346 routeInfo, 1347 null, 1348 ) 1349 1350 // THEN it succeeds 1351 val chipbarView = getChipbarView() 1352 assertThat(chipbarView.getChipText()) 1353 .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText()) 1354 } 1355 1356 /** Regression test for b/266218672. */ 1357 @Test 1358 fun receiverSucceededThenTimedOut_internalStateResetAndCanDisplayReceiverTriggered() { 1359 displayReceiverTriggered() 1360 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1361 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 1362 routeInfo, 1363 null, 1364 ) 1365 1366 fakeClock.advanceTime(TIMEOUT + 1L) 1367 verify(windowManager).removeView(any()) 1368 1369 reset(windowManager) 1370 1371 // WHEN we try to show RECEIVER_TRIGGERED 1372 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1373 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, 1374 routeInfo, 1375 null, 1376 ) 1377 1378 // THEN it succeeds 1379 val chipbarView = getChipbarView() 1380 assertThat(chipbarView.getChipText()) 1381 .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) 1382 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) 1383 } 1384 1385 /** Regression test for b/266218672. */ 1386 @Test 1387 fun toThisDeviceSucceededThenTimedOut_internalStateResetAndCanDisplayAlmostCloseToStart() { 1388 displayThisDeviceTriggered() 1389 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1390 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 1391 routeInfo, 1392 null, 1393 ) 1394 1395 fakeClock.advanceTime(TIMEOUT + 1L) 1396 verify(windowManager).removeView(any()) 1397 1398 reset(windowManager) 1399 1400 // WHEN we try to show ALMOST_CLOSE_TO_START 1401 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1402 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 1403 routeInfo, 1404 null, 1405 ) 1406 1407 // THEN it succeeds 1408 val chipbarView = getChipbarView() 1409 assertThat(chipbarView.getChipText()) 1410 .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) 1411 } 1412 1413 /** Regression test for b/266218672. */ 1414 @Test 1415 fun toThisDeviceSucceededThenTimedOut_internalStateResetAndCanDisplayThisDeviceTriggered() { 1416 displayThisDeviceTriggered() 1417 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1418 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, 1419 routeInfo, 1420 null, 1421 ) 1422 1423 fakeClock.advanceTime(TIMEOUT + 1L) 1424 verify(windowManager).removeView(any()) 1425 1426 reset(windowManager) 1427 1428 // WHEN we try to show THIS_DEVICE_TRIGGERED 1429 val newRouteInfo = 1430 MediaRoute2Info.Builder(DEFAULT_ID, "New Name") 1431 .addFeature("feature") 1432 .setClientPackageName(PACKAGE_NAME) 1433 .build() 1434 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1435 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 1436 newRouteInfo, 1437 null, 1438 ) 1439 1440 // THEN it succeeds 1441 val chipbarView = getChipbarView() 1442 assertThat(chipbarView.getChipText()) 1443 .isEqualTo( 1444 ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText("New Name") 1445 ) 1446 assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) 1447 } 1448 1449 @Test 1450 fun almostClose_hasLongTimeout_eventuallyTimesOut() { 1451 whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenAnswer { 1452 it.arguments[0] 1453 } 1454 1455 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1456 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, 1457 routeInfo, 1458 null, 1459 ) 1460 1461 // WHEN the default timeout has passed 1462 fakeClock.advanceTime(defaultTimeout + 1L) 1463 1464 // THEN the view is still on-screen because it has a long timeout 1465 verify(windowManager, never()).removeView(any()) 1466 1467 // WHEN a very long amount of time has passed 1468 fakeClock.advanceTime(5L * defaultTimeout) 1469 1470 // THEN the view does time out 1471 verify(windowManager).removeView(any()) 1472 } 1473 1474 @Test 1475 fun loading_hasLongTimeout_eventuallyTimesOut() { 1476 whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenAnswer { 1477 it.arguments[0] 1478 } 1479 1480 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1481 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 1482 routeInfo, 1483 null, 1484 ) 1485 1486 // WHEN the default timeout has passed 1487 fakeClock.advanceTime(defaultTimeout + 1L) 1488 1489 // THEN the view is still on-screen because it has a long timeout 1490 verify(windowManager, never()).removeView(any()) 1491 1492 // WHEN a very long amount of time has passed 1493 fakeClock.advanceTime(5L * defaultTimeout) 1494 1495 // THEN the view does time out 1496 verify(windowManager).removeView(any()) 1497 } 1498 1499 @Test 1500 fun succeeded_hasDefaultTimeout() { 1501 whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenAnswer { 1502 it.arguments[0] 1503 } 1504 1505 displayReceiverTriggered() 1506 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1507 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, 1508 routeInfo, 1509 null, 1510 ) 1511 1512 fakeClock.advanceTime(defaultTimeout + 1L) 1513 1514 verify(windowManager).removeView(any()) 1515 } 1516 1517 @Test 1518 fun failed_hasDefaultTimeout() { 1519 whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenAnswer { 1520 it.arguments[0] 1521 } 1522 1523 displayThisDeviceTriggered() 1524 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1525 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED, 1526 routeInfo, 1527 null, 1528 ) 1529 1530 fakeClock.advanceTime(defaultTimeout + 1L) 1531 1532 verify(windowManager).removeView(any()) 1533 } 1534 1535 private fun getChipbarView(): ViewGroup { 1536 val viewCaptor = ArgumentCaptor.forClass(View::class.java) 1537 verify(windowManager).addView(viewCaptor.capture(), any()) 1538 return viewCaptor.value as ViewGroup 1539 } 1540 1541 private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.start_icon) 1542 1543 private fun ViewGroup.getChipText(): String = 1544 (this.requireViewById<TextView>(R.id.text)).text as String 1545 1546 private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) 1547 1548 private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) 1549 1550 private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button) 1551 1552 private fun ChipStateSender.getExpectedStateText( 1553 otherDeviceName: String = OTHER_DEVICE_NAME, 1554 ): String? { 1555 return this.getChipTextString(context, otherDeviceName).loadText(context) 1556 } 1557 1558 // display receiver triggered state helper method to make sure we start from a valid state 1559 // transition (FAR_FROM_RECEIVER -> TRANSFER_TO_RECEIVER_TRIGGERED). 1560 private fun displayReceiverTriggered() { 1561 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1562 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, 1563 routeInfo, 1564 null 1565 ) 1566 } 1567 1568 // display this device triggered state helper method to make sure we start from a valid state 1569 // transition (FAR_FROM_RECEIVER -> TRANSFER_TO_THIS_DEVICE_TRIGGERED). 1570 private fun displayThisDeviceTriggered() { 1571 commandQueueCallback.updateMediaTapToTransferSenderDisplay( 1572 StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, 1573 routeInfo, 1574 null 1575 ) 1576 } 1577 1578 private fun setCommandQueueCallback() { 1579 val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>() 1580 verify(commandQueue).addCallback(capture(callbackCaptor)) 1581 commandQueueCallback = callbackCaptor.value 1582 reset(commandQueue) 1583 } 1584 } 1585 1586 private const val DEFAULT_ID = "defaultId" 1587 private const val APP_NAME = "Fake app name" 1588 private const val OTHER_DEVICE_NAME = "My Tablet" 1589 private const val BLANK_DEVICE_NAME = " " 1590 private const val PACKAGE_NAME = "com.android.systemui" 1591 private const val TIMEOUT = 10000 1592 1593 private val routeInfo = 1594 MediaRoute2Info.Builder(DEFAULT_ID, OTHER_DEVICE_NAME) 1595 .addFeature("feature") 1596 .setClientPackageName(PACKAGE_NAME) 1597 .build() 1598 1599 private val routeInfoWithBlankDeviceName = 1600 MediaRoute2Info.Builder(DEFAULT_ID, BLANK_DEVICE_NAME) 1601 .addFeature("feature") 1602 .setClientPackageName(PACKAGE_NAME) 1603 .build() 1604