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