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