1 /*
2  * Copyright (C) 2020 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.controls.ui.viewmodel
18 
19 import android.media.MediaMetadata
20 import android.media.session.MediaController
21 import android.media.session.MediaSession
22 import android.media.session.PlaybackState
23 import android.testing.TestableLooper
24 import android.view.MotionEvent
25 import android.widget.SeekBar
26 import androidx.arch.core.executor.ArchTaskExecutor
27 import androidx.arch.core.executor.TaskExecutor
28 import androidx.test.ext.junit.runners.AndroidJUnit4
29 import androidx.test.filters.SmallTest
30 import com.android.systemui.SysuiTestCase
31 import com.android.systemui.classifier.Classifier
32 import com.android.systemui.plugins.FalsingManager
33 import com.android.systemui.util.concurrency.FakeExecutor
34 import com.android.systemui.util.concurrency.FakeRepeatableExecutor
35 import com.android.systemui.util.time.FakeSystemClock
36 import com.google.common.truth.Truth.assertThat
37 import org.junit.After
38 import org.junit.Before
39 import org.junit.Ignore
40 import org.junit.Rule
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 import org.mockito.ArgumentCaptor
44 import org.mockito.Mock
45 import org.mockito.Mockito.any
46 import org.mockito.Mockito.anyInt
47 import org.mockito.Mockito.eq
48 import org.mockito.Mockito.mock
49 import org.mockito.Mockito.never
50 import org.mockito.Mockito.times
51 import org.mockito.Mockito.verify
52 import org.mockito.Mockito.`when` as whenever
53 import org.mockito.junit.MockitoJUnit
54 
55 @SmallTest
56 @RunWith(AndroidJUnit4::class)
57 @TestableLooper.RunWithLooper(setAsMainLooper = true)
58 public class SeekBarViewModelTest : SysuiTestCase() {
59 
60     private lateinit var viewModel: SeekBarViewModel
61     private lateinit var fakeExecutor: FakeExecutor
62     private val taskExecutor: TaskExecutor =
63         object : TaskExecutor() {
executeOnDiskIOnull64             override fun executeOnDiskIO(runnable: Runnable) {
65                 runnable.run()
66             }
postToMainThreadnull67             override fun postToMainThread(runnable: Runnable) {
68                 runnable.run()
69             }
isMainThreadnull70             override fun isMainThread(): Boolean {
71                 return true
72             }
73         }
74     @Mock private lateinit var mockController: MediaController
75     @Mock private lateinit var mockTransport: MediaController.TransportControls
76     @Mock private lateinit var falsingManager: FalsingManager
77     @Mock private lateinit var mockBar: SeekBar
78     private val token1 = MediaSession.Token(1, null)
79     private val token2 = MediaSession.Token(2, null)
80 
81     @JvmField @Rule val mockito = MockitoJUnit.rule()
82 
83     @Before
setUpnull84     fun setUp() {
85         fakeExecutor = FakeExecutor(FakeSystemClock())
86         viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager)
87         viewModel.logSeek = {}
88         whenever(mockController.sessionToken).thenReturn(token1)
89         whenever(mockBar.context).thenReturn(context)
90 
91         // LiveData to run synchronously
92         ArchTaskExecutor.getInstance().setDelegate(taskExecutor)
93     }
94 
95     @After
tearDownnull96     fun tearDown() {
97         ArchTaskExecutor.getInstance().setDelegate(null)
98     }
99 
100     @Test
updateRegistersCallbacknull101     fun updateRegistersCallback() {
102         viewModel.updateController(mockController)
103         verify(mockController).registerCallback(any())
104     }
105 
106     @Test
updateSecondTimeDoesNotRepeatRegistrationnull107     fun updateSecondTimeDoesNotRepeatRegistration() {
108         viewModel.updateController(mockController)
109         viewModel.updateController(mockController)
110         verify(mockController, times(1)).registerCallback(any())
111     }
112 
113     @Test
updateDifferentControllerUnregistersCallbacknull114     fun updateDifferentControllerUnregistersCallback() {
115         viewModel.updateController(mockController)
116         viewModel.updateController(mock(MediaController::class.java))
117         verify(mockController).unregisterCallback(any())
118     }
119 
120     @Test
updateDifferentControllerRegistersCallbacknull121     fun updateDifferentControllerRegistersCallback() {
122         viewModel.updateController(mockController)
123         val controller2 = mock(MediaController::class.java)
124         whenever(controller2.sessionToken).thenReturn(token2)
125         viewModel.updateController(controller2)
126         verify(controller2).registerCallback(any())
127     }
128 
129     @Test
updateToNullUnregistersCallbacknull130     fun updateToNullUnregistersCallback() {
131         viewModel.updateController(mockController)
132         viewModel.updateController(null)
133         verify(mockController).unregisterCallback(any())
134     }
135 
136     @Test
137     @Ignore
updateDurationWithPlaybacknull138     fun updateDurationWithPlayback() {
139         // GIVEN that the duration is contained within the metadata
140         val duration = 12000L
141         val metadata =
142             MediaMetadata.Builder().run {
143                 putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
144                 build()
145             }
146         whenever(mockController.getMetadata()).thenReturn(metadata)
147         // AND a valid playback state (ie. media session is not destroyed)
148         val state =
149             PlaybackState.Builder().run {
150                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
151                 build()
152             }
153         whenever(mockController.getPlaybackState()).thenReturn(state)
154         // WHEN the controller is updated
155         viewModel.updateController(mockController)
156         // THEN the duration is extracted
157         assertThat(viewModel.progress.value!!.duration).isEqualTo(duration)
158         assertThat(viewModel.progress.value!!.enabled).isTrue()
159     }
160 
161     @Test
162     @Ignore
updateDurationWithoutPlaybacknull163     fun updateDurationWithoutPlayback() {
164         // GIVEN that the duration is contained within the metadata
165         val duration = 12000L
166         val metadata =
167             MediaMetadata.Builder().run {
168                 putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
169                 build()
170             }
171         whenever(mockController.getMetadata()).thenReturn(metadata)
172         // WHEN the controller is updated
173         viewModel.updateController(mockController)
174         // THEN the duration is extracted
175         assertThat(viewModel.progress.value!!.duration).isEqualTo(duration)
176         assertThat(viewModel.progress.value!!.enabled).isFalse()
177     }
178 
179     @Test
updateDurationNegativenull180     fun updateDurationNegative() {
181         // GIVEN that the duration is negative
182         val duration = -1L
183         val metadata =
184             MediaMetadata.Builder().run {
185                 putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
186                 build()
187             }
188         whenever(mockController.getMetadata()).thenReturn(metadata)
189         // AND a valid playback state (ie. media session is not destroyed)
190         val state =
191             PlaybackState.Builder().run {
192                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
193                 build()
194             }
195         whenever(mockController.getPlaybackState()).thenReturn(state)
196         // WHEN the controller is updated
197         viewModel.updateController(mockController)
198         // THEN the seek bar is disabled
199         assertThat(viewModel.progress.value!!.enabled).isFalse()
200     }
201 
202     @Test
updateDurationZeronull203     fun updateDurationZero() {
204         // GIVEN that the duration is zero
205         val duration = 0L
206         val metadata =
207             MediaMetadata.Builder().run {
208                 putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
209                 build()
210             }
211         whenever(mockController.getMetadata()).thenReturn(metadata)
212         // AND a valid playback state (ie. media session is not destroyed)
213         val state =
214             PlaybackState.Builder().run {
215                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
216                 build()
217             }
218         whenever(mockController.getPlaybackState()).thenReturn(state)
219         // WHEN the controller is updated
220         viewModel.updateController(mockController)
221         // THEN the seek bar is disabled
222         assertThat(viewModel.progress.value!!.enabled).isFalse()
223     }
224 
225     @Test
226     @Ignore
updateDurationNoMetadatanull227     fun updateDurationNoMetadata() {
228         // GIVEN that the metadata is null
229         whenever(mockController.getMetadata()).thenReturn(null)
230         // AND a valid playback state (ie. media session is not destroyed)
231         val state =
232             PlaybackState.Builder().run {
233                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
234                 build()
235             }
236         whenever(mockController.getPlaybackState()).thenReturn(state)
237         // WHEN the controller is updated
238         viewModel.updateController(mockController)
239         // THEN the seek bar is disabled
240         assertThat(viewModel.progress.value!!.enabled).isFalse()
241     }
242 
243     @Test
updateElapsedTimenull244     fun updateElapsedTime() {
245         // GIVEN that the PlaybackState contains the current position
246         val position = 200L
247         val state =
248             PlaybackState.Builder().run {
249                 setState(PlaybackState.STATE_PLAYING, position, 1f)
250                 build()
251             }
252         whenever(mockController.getPlaybackState()).thenReturn(state)
253         // WHEN the controller is updated
254         viewModel.updateController(mockController)
255         // THEN elapsed time is captured
256         assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(200.toInt())
257     }
258 
259     @Test
260     @Ignore
updateSeekAvailablenull261     fun updateSeekAvailable() {
262         // GIVEN that seek is included in actions
263         val state =
264             PlaybackState.Builder().run {
265                 setActions(PlaybackState.ACTION_SEEK_TO)
266                 build()
267             }
268         whenever(mockController.getPlaybackState()).thenReturn(state)
269         // WHEN the controller is updated
270         viewModel.updateController(mockController)
271         // THEN seek is available
272         assertThat(viewModel.progress.value!!.seekAvailable).isTrue()
273     }
274 
275     @Test
276     @Ignore
updateSeekNotAvailablenull277     fun updateSeekNotAvailable() {
278         // GIVEN that seek is not included in actions
279         val state =
280             PlaybackState.Builder().run {
281                 setActions(PlaybackState.ACTION_PLAY)
282                 build()
283             }
284         whenever(mockController.getPlaybackState()).thenReturn(state)
285         // WHEN the controller is updated
286         viewModel.updateController(mockController)
287         // THEN seek is not available
288         assertThat(viewModel.progress.value!!.seekAvailable).isFalse()
289     }
290 
291     @Test
onSeeknull292     fun onSeek() {
293         whenever(mockController.getTransportControls()).thenReturn(mockTransport)
294         viewModel.updateController(mockController)
295         // WHEN user input is dispatched
296         val pos = 42L
297         viewModel.onSeek(pos)
298         fakeExecutor.runAllReady()
299         // THEN transport controls should be used
300         verify(mockTransport).seekTo(pos)
301     }
302 
303     @Test
onSeekWithFalsenull304     fun onSeekWithFalse() {
305         whenever(mockController.getTransportControls()).thenReturn(mockTransport)
306         viewModel.updateController(mockController)
307         // WHEN a false is received during the seek gesture
308         val pos = 42L
309         with(viewModel) {
310             onSeekStarting()
311             onSeekFalse()
312             onSeek(pos)
313         }
314         fakeExecutor.runAllReady()
315         // THEN the seek is rejected and the transport never receives seekTo
316         verify(mockTransport, never()).seekTo(pos)
317     }
318 
319     @Test
onSeekProgressnull320     fun onSeekProgress() {
321         val pos = 42L
322         with(viewModel) {
323             onSeekStarting()
324             onSeekProgress(pos)
325         }
326         fakeExecutor.runAllReady()
327         // THEN then elapsed time should be updated
328         assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(pos)
329     }
330 
331     @Test
332     @Ignore
onSeekProgressWithSeekStartingnull333     fun onSeekProgressWithSeekStarting() {
334         val pos = 42L
335         with(viewModel) { onSeekProgress(pos) }
336         fakeExecutor.runAllReady()
337         // THEN then elapsed time should not be updated
338         assertThat(viewModel.progress.value!!.elapsedTime).isNull()
339     }
340 
341     @Test
seekStarted_listenerNotifiednull342     fun seekStarted_listenerNotified() {
343         var isScrubbing: Boolean? = null
344         val listener =
345             object : SeekBarViewModel.ScrubbingChangeListener {
346                 override fun onScrubbingChanged(scrubbing: Boolean) {
347                     isScrubbing = scrubbing
348                 }
349             }
350         viewModel.setScrubbingChangeListener(listener)
351 
352         viewModel.onSeekStarting()
353         fakeExecutor.runAllReady()
354 
355         assertThat(isScrubbing).isTrue()
356     }
357 
358     @Test
seekEnded_listenerNotifiednull359     fun seekEnded_listenerNotified() {
360         var isScrubbing: Boolean? = null
361         val listener =
362             object : SeekBarViewModel.ScrubbingChangeListener {
363                 override fun onScrubbingChanged(scrubbing: Boolean) {
364                     isScrubbing = scrubbing
365                 }
366             }
367         viewModel.setScrubbingChangeListener(listener)
368 
369         // Start seeking
370         viewModel.onSeekStarting()
371         fakeExecutor.runAllReady()
372         // End seeking
373         viewModel.onSeek(15L)
374         fakeExecutor.runAllReady()
375 
376         assertThat(isScrubbing).isFalse()
377     }
378 
379     @Test
380     @Ignore
onProgressChangedFromUsernull381     fun onProgressChangedFromUser() {
382         // WHEN user starts dragging the seek bar
383         val pos = 42
384         val bar = SeekBar(context)
385         with(viewModel.seekBarListener) {
386             onStartTrackingTouch(bar)
387             onProgressChanged(bar, pos, true)
388         }
389         fakeExecutor.runAllReady()
390         // THEN then elapsed time should be updated
391         assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(pos)
392     }
393 
394     @Test
onProgressChangedFromUserWithoutStartTrackingTouch_transportUpdatednull395     fun onProgressChangedFromUserWithoutStartTrackingTouch_transportUpdated() {
396         whenever(mockController.transportControls).thenReturn(mockTransport)
397         viewModel.updateController(mockController)
398         val pos = 42
399         val bar = SeekBar(context)
400 
401         // WHEN we get an onProgressChanged event without an onStartTrackingTouch event
402         with(viewModel.seekBarListener) { onProgressChanged(bar, pos, true) }
403         fakeExecutor.runAllReady()
404 
405         // THEN we immediately update the transport
406         verify(mockTransport).seekTo(pos.toLong())
407     }
408 
409     @Test
onProgressChangedNotFromUsernull410     fun onProgressChangedNotFromUser() {
411         whenever(mockController.getTransportControls()).thenReturn(mockTransport)
412         viewModel.updateController(mockController)
413         // WHEN user starts dragging the seek bar
414         val pos = 42
415         viewModel.seekBarListener.onProgressChanged(SeekBar(context), pos, false)
416         fakeExecutor.runAllReady()
417         // THEN transport controls should be used
418         verify(mockTransport, never()).seekTo(pos.toLong())
419     }
420 
421     @Test
onStartTrackingTouchnull422     fun onStartTrackingTouch() {
423         whenever(mockController.getTransportControls()).thenReturn(mockTransport)
424         viewModel.updateController(mockController)
425         // WHEN user starts dragging the seek bar
426         val pos = 42
427         val bar = SeekBar(context).apply { progress = pos }
428         viewModel.seekBarListener.onStartTrackingTouch(bar)
429         fakeExecutor.runAllReady()
430         // THEN transport controls should be used
431         verify(mockTransport, never()).seekTo(pos.toLong())
432     }
433 
434     @Test
onStopTrackingTouchnull435     fun onStopTrackingTouch() {
436         whenever(mockController.getTransportControls()).thenReturn(mockTransport)
437         viewModel.updateController(mockController)
438         // WHEN user ends drag
439         val pos = 42
440         val bar = SeekBar(context).apply { progress = pos }
441         viewModel.seekBarListener.onStopTrackingTouch(bar)
442         fakeExecutor.runAllReady()
443         // THEN transport controls should be used
444         verify(mockTransport).seekTo(pos.toLong())
445     }
446 
447     @Test
onStopTrackingTouchAfterProgressnull448     fun onStopTrackingTouchAfterProgress() {
449         whenever(mockController.getTransportControls()).thenReturn(mockTransport)
450         viewModel.updateController(mockController)
451         // WHEN user starts dragging the seek bar
452         val pos = 42
453         val progPos = 84
454         val bar = SeekBar(context).apply { progress = pos }
455         with(viewModel.seekBarListener) {
456             onStartTrackingTouch(bar)
457             onProgressChanged(bar, progPos, true)
458             onStopTrackingTouch(bar)
459         }
460         fakeExecutor.runAllReady()
461         // THEN then elapsed time should be updated
462         verify(mockTransport).seekTo(eq(pos.toLong()))
463     }
464 
465     @Test
onFalseTapOrTouchnull466     fun onFalseTapOrTouch() {
467         whenever(mockController.getTransportControls()).thenReturn(mockTransport)
468         whenever(falsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).thenReturn(true)
469         whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
470 
471         viewModel.updateController(mockController)
472         val pos = 40
473         val bar = SeekBar(context).apply { progress = pos }
474         with(viewModel.seekBarListener) {
475             onStartTrackingTouch(bar)
476             onStopTrackingTouch(bar)
477         }
478         fakeExecutor.runAllReady()
479 
480         // THEN transport controls should not be used
481         verify(mockTransport, never()).seekTo(pos.toLong())
482     }
483 
484     @Test
onSeekbarGrabInvalidTouchnull485     fun onSeekbarGrabInvalidTouch() {
486         whenever(mockController.getTransportControls()).thenReturn(mockTransport)
487         viewModel.firstMotionEvent =
488             MotionEvent.obtain(12L, 13L, MotionEvent.ACTION_DOWN, 76F, 0F, 0)
489         viewModel.lastMotionEvent = MotionEvent.obtain(12L, 14L, MotionEvent.ACTION_UP, 78F, 4F, 0)
490         val pos = 78
491 
492         viewModel.updateController(mockController)
493         // WHEN user ends drag
494         val bar = SeekBar(context).apply { progress = pos }
495         with(viewModel.seekBarListener) {
496             onStartTrackingTouch(bar)
497             onStopTrackingTouch(bar)
498         }
499         fakeExecutor.runAllReady()
500 
501         // THEN transport controls should not be used
502         verify(mockTransport, never()).seekTo(pos.toLong())
503     }
504 
505     @Test
onSeekbarGrabValidTouchnull506     fun onSeekbarGrabValidTouch() {
507         whenever(mockController.transportControls).thenReturn(mockTransport)
508         viewModel.firstMotionEvent =
509             MotionEvent.obtain(12L, 13L, MotionEvent.ACTION_DOWN, 36F, 0F, 0)
510         viewModel.lastMotionEvent = MotionEvent.obtain(12L, 14L, MotionEvent.ACTION_UP, 40F, 1F, 0)
511         val pos = 40
512 
513         viewModel.updateController(mockController)
514         // WHEN user ends drag
515         val bar = SeekBar(context).apply { progress = pos }
516         with(viewModel.seekBarListener) {
517             onStartTrackingTouch(bar)
518             onStopTrackingTouch(bar)
519         }
520         fakeExecutor.runAllReady()
521 
522         // THEN transport controls should be used
523         verify(mockTransport).seekTo(pos.toLong())
524     }
525 
526     @Test
queuePollTaskWhenPlayingnull527     fun queuePollTaskWhenPlaying() {
528         // GIVEN that the track is playing
529         val state =
530             PlaybackState.Builder().run {
531                 setState(PlaybackState.STATE_PLAYING, 100L, 1f)
532                 build()
533             }
534         whenever(mockController.getPlaybackState()).thenReturn(state)
535         // WHEN the controller is updated
536         viewModel.updateController(mockController)
537         // THEN a task is queued
538         assertThat(fakeExecutor.numPending()).isEqualTo(1)
539     }
540 
541     @Test
noQueuePollTaskWhenStoppednull542     fun noQueuePollTaskWhenStopped() {
543         // GIVEN that the playback state is stopped
544         val state =
545             PlaybackState.Builder().run {
546                 setState(PlaybackState.STATE_STOPPED, 200L, 1f)
547                 build()
548             }
549         whenever(mockController.getPlaybackState()).thenReturn(state)
550         // WHEN updated
551         viewModel.updateController(mockController)
552         // THEN an update task is not queued
553         assertThat(fakeExecutor.numPending()).isEqualTo(0)
554     }
555 
556     @Test
queuePollTaskWhenListeningnull557     fun queuePollTaskWhenListening() {
558         // GIVEN listening
559         viewModel.listening = true
560         with(fakeExecutor) {
561             advanceClockToNext()
562             runAllReady()
563         }
564         // AND the playback state is playing
565         val state =
566             PlaybackState.Builder().run {
567                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
568                 build()
569             }
570         whenever(mockController.getPlaybackState()).thenReturn(state)
571         // WHEN updated
572         viewModel.updateController(mockController)
573         // THEN an update task is queued
574         assertThat(fakeExecutor.numPending()).isEqualTo(1)
575     }
576 
577     @Test
noQueuePollTaskWhenNotListeningnull578     fun noQueuePollTaskWhenNotListening() {
579         // GIVEN not listening
580         viewModel.listening = false
581         with(fakeExecutor) {
582             advanceClockToNext()
583             runAllReady()
584         }
585         // AND the playback state is playing
586         val state =
587             PlaybackState.Builder().run {
588                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
589                 build()
590             }
591         whenever(mockController.getPlaybackState()).thenReturn(state)
592         // WHEN updated
593         viewModel.updateController(mockController)
594         // THEN an update task is not queued
595         assertThat(fakeExecutor.numPending()).isEqualTo(0)
596     }
597 
598     @Test
pollTaskQueuesAnotherPollTaskWhenPlayingnull599     fun pollTaskQueuesAnotherPollTaskWhenPlaying() {
600         // GIVEN that the track is playing
601         val state =
602             PlaybackState.Builder().run {
603                 setState(PlaybackState.STATE_PLAYING, 100L, 1f)
604                 build()
605             }
606         whenever(mockController.getPlaybackState()).thenReturn(state)
607         viewModel.updateController(mockController)
608         // WHEN the next task runs
609         with(fakeExecutor) {
610             advanceClockToNext()
611             runAllReady()
612         }
613         // THEN another task is queued
614         assertThat(fakeExecutor.numPending()).isEqualTo(1)
615     }
616 
617     @Test
noQueuePollTaskWhenSeekingnull618     fun noQueuePollTaskWhenSeeking() {
619         // GIVEN listening
620         viewModel.listening = true
621         // AND the playback state is playing
622         val state =
623             PlaybackState.Builder().run {
624                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
625                 build()
626             }
627         whenever(mockController.getPlaybackState()).thenReturn(state)
628         viewModel.updateController(mockController)
629         with(fakeExecutor) {
630             advanceClockToNext()
631             runAllReady()
632         }
633         // WHEN seek starts
634         viewModel.onSeekStarting()
635         with(fakeExecutor) {
636             advanceClockToNext()
637             runAllReady()
638         }
639         // THEN an update task is not queued because we don't want it fighting with the user when
640         // they are trying to move the thumb.
641         assertThat(fakeExecutor.numPending()).isEqualTo(0)
642     }
643 
644     @Test
queuePollTaskWhenDoneSeekingWithFalsenull645     fun queuePollTaskWhenDoneSeekingWithFalse() {
646         // GIVEN listening
647         viewModel.listening = true
648         // AND the playback state is playing
649         val state =
650             PlaybackState.Builder().run {
651                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
652                 build()
653             }
654         whenever(mockController.getPlaybackState()).thenReturn(state)
655         viewModel.updateController(mockController)
656         with(fakeExecutor) {
657             advanceClockToNext()
658             runAllReady()
659         }
660         // WHEN seek finishes after a false
661         with(viewModel) {
662             onSeekStarting()
663             onSeekFalse()
664             onSeek(42L)
665         }
666         with(fakeExecutor) {
667             advanceClockToNext()
668             runAllReady()
669         }
670         // THEN an update task is queued because the gesture was ignored and progress was restored.
671         assertThat(fakeExecutor.numPending()).isEqualTo(1)
672     }
673 
674     @Test
noQueuePollTaskWhenDoneSeekingnull675     fun noQueuePollTaskWhenDoneSeeking() {
676         // GIVEN listening
677         viewModel.listening = true
678         // AND the playback state is playing
679         val state =
680             PlaybackState.Builder().run {
681                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
682                 build()
683             }
684         whenever(mockController.getPlaybackState()).thenReturn(state)
685         viewModel.updateController(mockController)
686         with(fakeExecutor) {
687             advanceClockToNext()
688             runAllReady()
689         }
690         // WHEN seek finishes after a false
691         with(viewModel) {
692             onSeekStarting()
693             onSeek(42L)
694         }
695         with(fakeExecutor) {
696             advanceClockToNext()
697             runAllReady()
698         }
699         // THEN no update task is queued because we are waiting for an updated playback state to be
700         // returned in response to the seek.
701         assertThat(fakeExecutor.numPending()).isEqualTo(0)
702     }
703 
704     @Test
startListeningQueuesPollTasknull705     fun startListeningQueuesPollTask() {
706         // GIVEN not listening
707         viewModel.listening = false
708         with(fakeExecutor) {
709             advanceClockToNext()
710             runAllReady()
711         }
712         // AND the playback state is playing
713         val state =
714             PlaybackState.Builder().run {
715                 setState(PlaybackState.STATE_STOPPED, 200L, 1f)
716                 build()
717             }
718         whenever(mockController.getPlaybackState()).thenReturn(state)
719         viewModel.updateController(mockController)
720         // WHEN start listening
721         viewModel.listening = true
722         // THEN an update task is queued
723         assertThat(fakeExecutor.numPending()).isEqualTo(1)
724     }
725 
726     @Test
playbackChangeQueuesPollTasknull727     fun playbackChangeQueuesPollTask() {
728         viewModel.updateController(mockController)
729         val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
730         verify(mockController).registerCallback(captor.capture())
731         val callback = captor.value
732         // WHEN the callback receives an new state
733         val state =
734             PlaybackState.Builder().run {
735                 setState(PlaybackState.STATE_PLAYING, 100L, 1f)
736                 build()
737             }
738         callback.onPlaybackStateChanged(state)
739         with(fakeExecutor) {
740             advanceClockToNext()
741             runAllReady()
742         }
743         // THEN an update task is queued
744         assertThat(fakeExecutor.numPending()).isEqualTo(1)
745     }
746 
747     @Test
748     @Ignore
clearSeekBarnull749     fun clearSeekBar() {
750         // GIVEN that the duration is contained within the metadata
751         val metadata =
752             MediaMetadata.Builder().run {
753                 putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L)
754                 build()
755             }
756         whenever(mockController.getMetadata()).thenReturn(metadata)
757         // AND a valid playback state (ie. media session is not destroyed)
758         val state =
759             PlaybackState.Builder().run {
760                 setState(PlaybackState.STATE_PLAYING, 200L, 1f)
761                 build()
762             }
763         whenever(mockController.getPlaybackState()).thenReturn(state)
764         // AND the controller has been updated
765         viewModel.updateController(mockController)
766         // WHEN the controller is cleared on the event when the session is destroyed
767         viewModel.clearController()
768         with(fakeExecutor) {
769             advanceClockToNext()
770             runAllReady()
771         }
772         // THEN the seek bar is disabled
773         assertThat(viewModel.progress.value!!.enabled).isFalse()
774     }
775 
776     @Test
clearSeekBarUnregistersCallbacknull777     fun clearSeekBarUnregistersCallback() {
778         viewModel.updateController(mockController)
779         viewModel.clearController()
780         fakeExecutor.runAllReady()
781         verify(mockController).unregisterCallback(any())
782     }
783 
784     @Test
destroyUnregistersCallbacknull785     fun destroyUnregistersCallback() {
786         viewModel.updateController(mockController)
787         viewModel.onDestroy()
788         fakeExecutor.runAllReady()
789         verify(mockController).unregisterCallback(any())
790     }
791 
792     @Test
nullPlaybackStateUnregistersCallbacknull793     fun nullPlaybackStateUnregistersCallback() {
794         viewModel.updateController(mockController)
795         val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
796         verify(mockController).registerCallback(captor.capture())
797         val callback = captor.value
798         // WHEN the callback receives a null state
799         callback.onPlaybackStateChanged(null)
800         with(fakeExecutor) {
801             advanceClockToNext()
802             runAllReady()
803         }
804         // THEN we unregister callback (as a result of clearing the controller)
805         fakeExecutor.runAllReady()
806         verify(mockController).unregisterCallback(any())
807     }
808 }
809