1 /* <lambda>null2 * 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.controls.ui.controller 18 19 import android.animation.AnimatorSet 20 import android.content.Context 21 import android.content.res.Configuration 22 import android.content.res.Configuration.ORIENTATION_LANDSCAPE 23 import android.graphics.drawable.Drawable 24 import android.graphics.drawable.GradientDrawable 25 import android.graphics.drawable.RippleDrawable 26 import android.testing.TestableLooper 27 import android.view.View 28 import android.view.ViewGroup 29 import android.view.animation.Interpolator 30 import android.widget.FrameLayout 31 import android.widget.ImageButton 32 import android.widget.ImageView 33 import android.widget.SeekBar 34 import android.widget.TextView 35 import androidx.constraintlayout.widget.ConstraintSet 36 import androidx.lifecycle.LiveData 37 import androidx.test.ext.junit.runners.AndroidJUnit4 38 import androidx.test.filters.SmallTest 39 import com.android.internal.widget.CachingIconView 40 import com.android.systemui.SysuiTestCase 41 import com.android.systemui.flags.EnableSceneContainer 42 import com.android.systemui.media.controls.ui.view.GutsViewHolder 43 import com.android.systemui.media.controls.ui.view.MediaHost 44 import com.android.systemui.media.controls.ui.view.MediaViewHolder 45 import com.android.systemui.media.controls.ui.view.RecommendationViewHolder 46 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel 47 import com.android.systemui.res.R 48 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView 49 import com.android.systemui.surfaceeffects.ripple.MultiRippleView 50 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView 51 import com.android.systemui.util.animation.MeasurementInput 52 import com.android.systemui.util.animation.TransitionLayout 53 import com.android.systemui.util.animation.TransitionViewState 54 import com.android.systemui.util.animation.WidgetState 55 import com.android.systemui.util.concurrency.FakeExecutor 56 import com.android.systemui.util.mockito.withArgCaptor 57 import com.android.systemui.util.settings.GlobalSettings 58 import com.android.systemui.util.time.FakeSystemClock 59 import com.google.common.truth.Truth.assertThat 60 import junit.framework.Assert.assertTrue 61 import org.junit.Before 62 import org.junit.Test 63 import org.junit.runner.RunWith 64 import org.mockito.ArgumentMatchers.floatThat 65 import org.mockito.Mock 66 import org.mockito.Mockito 67 import org.mockito.Mockito.never 68 import org.mockito.Mockito.times 69 import org.mockito.Mockito.verify 70 import org.mockito.Mockito.`when` as whenever 71 import org.mockito.MockitoAnnotations 72 73 @SmallTest 74 @TestableLooper.RunWithLooper(setAsMainLooper = true) 75 @RunWith(AndroidJUnit4::class) 76 class MediaViewControllerTest : SysuiTestCase() { 77 private val mediaHostStateHolder = MediaHost.MediaHostStateHolder() 78 private val mediaHostStatesManager = MediaHostStatesManager() 79 private val configurationController = 80 com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context) 81 private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) 82 private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) 83 private val clock = FakeSystemClock() 84 private lateinit var mainExecutor: FakeExecutor 85 private lateinit var seekBar: SeekBar 86 private lateinit var multiRippleView: MultiRippleView 87 private lateinit var turbulenceNoiseView: TurbulenceNoiseView 88 private lateinit var loadingEffectView: LoadingEffectView 89 private lateinit var settings: ImageButton 90 private lateinit var cancel: View 91 private lateinit var cancelText: TextView 92 private lateinit var dismiss: FrameLayout 93 private lateinit var dismissText: TextView 94 private lateinit var titleText: TextView 95 private lateinit var artistText: TextView 96 private lateinit var explicitIndicator: CachingIconView 97 private lateinit var seamless: ViewGroup 98 private lateinit var seamlessButton: View 99 private lateinit var seamlessIcon: ImageView 100 private lateinit var seamlessText: TextView 101 private lateinit var scrubbingElapsedTimeView: TextView 102 private lateinit var scrubbingTotalTimeView: TextView 103 private lateinit var actionPlayPause: ImageButton 104 private lateinit var actionNext: ImageButton 105 private lateinit var actionPrev: ImageButton 106 @Mock private lateinit var seamlessBackground: RippleDrawable 107 @Mock private lateinit var albumView: ImageView 108 @Mock lateinit var logger: MediaViewLogger 109 @Mock private lateinit var mockViewState: TransitionViewState 110 @Mock private lateinit var mockCopiedState: TransitionViewState 111 @Mock private lateinit var detailWidgetState: WidgetState 112 @Mock private lateinit var controlWidgetState: WidgetState 113 @Mock private lateinit var mediaTitleWidgetState: WidgetState 114 @Mock private lateinit var mediaSubTitleWidgetState: WidgetState 115 @Mock private lateinit var mediaContainerWidgetState: WidgetState 116 @Mock private lateinit var seekBarViewModel: SeekBarViewModel 117 @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress> 118 @Mock private lateinit var globalSettings: GlobalSettings 119 @Mock private lateinit var viewHolder: MediaViewHolder 120 @Mock private lateinit var view: TransitionLayout 121 @Mock private lateinit var mockAnimator: AnimatorSet 122 @Mock private lateinit var gutsViewHolder: GutsViewHolder 123 @Mock private lateinit var gutsText: TextView 124 125 private val delta = 0.1F 126 127 private lateinit var mediaViewController: MediaViewController 128 129 @Before 130 fun setup() { 131 MockitoAnnotations.initMocks(this) 132 mainExecutor = FakeExecutor(clock) 133 mediaViewController = 134 object : 135 MediaViewController( 136 context, 137 configurationController, 138 mediaHostStatesManager, 139 logger, 140 seekBarViewModel, 141 mainExecutor, 142 globalSettings, 143 ) { 144 override fun loadAnimator( 145 context: Context, 146 animId: Int, 147 motionInterpolator: Interpolator?, 148 vararg targets: View? 149 ): AnimatorSet { 150 return mockAnimator 151 } 152 } 153 initGutsViewHolderMocks() 154 initMediaViewHolderMocks() 155 } 156 157 @Test 158 fun testOrientationChanged_heightOfPlayerIsUpdated() { 159 val newConfig = Configuration() 160 161 mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) 162 // Change the height to see the effect of orientation change. 163 MediaViewHolder.backgroundIds.forEach { id -> 164 mediaViewController.expandedLayout.getConstraint(id).layout.mHeight = 10 165 } 166 newConfig.orientation = ORIENTATION_LANDSCAPE 167 configurationController.onConfigurationChanged(newConfig) 168 169 MediaViewHolder.backgroundIds.forEach { id -> 170 assertTrue( 171 mediaViewController.expandedLayout.getConstraint(id).layout.mHeight == 172 context.resources.getDimensionPixelSize( 173 R.dimen.qs_media_session_height_expanded 174 ) 175 ) 176 } 177 } 178 179 @Test 180 fun testOrientationChanged_heightOfRecCardIsUpdated() { 181 val newConfig = Configuration() 182 183 mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION) 184 // Change the height to see the effect of orientation change. 185 mediaViewController.expandedLayout 186 .getConstraint(RecommendationViewHolder.backgroundId) 187 .layout 188 .mHeight = 10 189 newConfig.orientation = ORIENTATION_LANDSCAPE 190 configurationController.onConfigurationChanged(newConfig) 191 192 assertTrue( 193 mediaViewController.expandedLayout 194 .getConstraint(RecommendationViewHolder.backgroundId) 195 .layout 196 .mHeight == 197 context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) 198 ) 199 } 200 201 @Test 202 fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() { 203 mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) 204 player.measureState = 205 TransitionViewState().apply { 206 this.height = 100 207 this.measureHeight = 100 208 } 209 mediaHostStateHolder.expansion = 1f 210 val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) 211 val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) 212 mediaHostStateHolder.measurementInput = 213 MeasurementInput(widthMeasureSpec, heightMeasureSpec) 214 215 // Test no squish 216 mediaHostStateHolder.squishFraction = 1f 217 assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100) 218 assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100) 219 220 // Test half squish 221 mediaHostStateHolder.squishFraction = 0.5f 222 assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50) 223 assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100) 224 } 225 226 @Test 227 fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() { 228 mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION) 229 recommendation.measureState = TransitionViewState().apply { this.height = 100 } 230 mediaHostStateHolder.expansion = 1f 231 val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) 232 val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) 233 mediaHostStateHolder.measurementInput = 234 MeasurementInput(widthMeasureSpec, heightMeasureSpec) 235 236 // Test no squish 237 mediaHostStateHolder.squishFraction = 1f 238 assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100) 239 assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100) 240 241 // Test half squish 242 mediaHostStateHolder.squishFraction = 0.5f 243 assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50) 244 assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100) 245 } 246 247 @Test 248 fun testObtainViewState_expandedMatchesParentHeight() { 249 mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) 250 player.measureState = 251 TransitionViewState().apply { 252 this.height = 100 253 this.measureHeight = 100 254 } 255 mediaHostStateHolder.expandedMatchesParentHeight = true 256 mediaHostStateHolder.expansion = 1f 257 mediaHostStateHolder.measurementInput = 258 MeasurementInput( 259 View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), 260 View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), 261 ) 262 263 // Assign the height of each expanded layout 264 MediaViewHolder.backgroundIds.forEach { id -> 265 mediaViewController.expandedLayout.getConstraint(id).layout.mHeight = 100 266 } 267 268 mediaViewController.obtainViewState(mediaHostStateHolder) 269 270 // Verify height of each expanded layout is updated to match constraint 271 MediaViewHolder.backgroundIds.forEach { id -> 272 assertTrue( 273 mediaViewController.expandedLayout.getConstraint(id).layout.mHeight == 274 ConstraintSet.MATCH_CONSTRAINT 275 ) 276 } 277 } 278 279 @Test 280 fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() { 281 whenever(mockViewState.copy()).thenReturn(mockCopiedState) 282 whenever(mockCopiedState.widgetStates) 283 .thenReturn( 284 mutableMapOf( 285 R.id.media_progress_bar to controlWidgetState, 286 R.id.header_artist to detailWidgetState 287 ) 288 ) 289 whenever(mockCopiedState.measureHeight).thenReturn(200) 290 // detail widgets occupy [90, 100] 291 whenever(detailWidgetState.y).thenReturn(90F) 292 whenever(detailWidgetState.height).thenReturn(10) 293 whenever(detailWidgetState.alpha).thenReturn(1F) 294 // control widgets occupy [150, 170] 295 whenever(controlWidgetState.y).thenReturn(150F) 296 whenever(controlWidgetState.height).thenReturn(20) 297 whenever(controlWidgetState.alpha).thenReturn(1F) 298 // in current bezier, when the progress reach 0.38, the result will be 0.5 299 mediaViewController.squishViewState(mockViewState, 181.4F / 200F) 300 verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } 301 verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } 302 mediaViewController.squishViewState(mockViewState, 200F / 200F) 303 verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } 304 verify(detailWidgetState, times(2)).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } 305 } 306 307 @Test 308 fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_invisibleElements() { 309 whenever(mockViewState.copy()).thenReturn(mockCopiedState) 310 whenever(mockCopiedState.widgetStates) 311 .thenReturn( 312 mutableMapOf( 313 R.id.media_progress_bar to controlWidgetState, 314 R.id.header_artist to detailWidgetState 315 ) 316 ) 317 whenever(mockCopiedState.measureHeight).thenReturn(200) 318 // detail widgets occupy [90, 100] 319 whenever(detailWidgetState.y).thenReturn(90F) 320 whenever(detailWidgetState.height).thenReturn(10) 321 whenever(detailWidgetState.alpha).thenReturn(0F) 322 // control widgets occupy [150, 170] 323 whenever(controlWidgetState.y).thenReturn(150F) 324 whenever(controlWidgetState.height).thenReturn(20) 325 whenever(controlWidgetState.alpha).thenReturn(0F) 326 // Verify that alpha remains 0 throughout squishing 327 mediaViewController.squishViewState(mockViewState, 181.4F / 200F) 328 verify(controlWidgetState, never()).alpha = floatThat { it > 0 } 329 verify(detailWidgetState, never()).alpha = floatThat { it > 0 } 330 mediaViewController.squishViewState(mockViewState, 200F / 200F) 331 verify(controlWidgetState, never()).alpha = floatThat { it > 0 } 332 verify(detailWidgetState, never()).alpha = floatThat { it > 0 } 333 } 334 335 @Test 336 fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() { 337 whenever(mockViewState.copy()).thenReturn(mockCopiedState) 338 whenever(mockCopiedState.widgetStates) 339 .thenReturn( 340 mutableMapOf( 341 R.id.media_title to mediaTitleWidgetState, 342 R.id.media_subtitle to mediaSubTitleWidgetState, 343 R.id.media_cover1_container to mediaContainerWidgetState 344 ) 345 ) 346 whenever(mockCopiedState.measureHeight).thenReturn(360) 347 // media container widgets occupy [20, 300] 348 whenever(mediaContainerWidgetState.y).thenReturn(20F) 349 whenever(mediaContainerWidgetState.height).thenReturn(280) 350 whenever(mediaContainerWidgetState.alpha).thenReturn(1F) 351 // media title widgets occupy [320, 330] 352 whenever(mediaTitleWidgetState.y).thenReturn(320F) 353 whenever(mediaTitleWidgetState.height).thenReturn(10) 354 whenever(mediaTitleWidgetState.alpha).thenReturn(1F) 355 // media subtitle widgets occupy [340, 350] 356 whenever(mediaSubTitleWidgetState.y).thenReturn(340F) 357 whenever(mediaSubTitleWidgetState.height).thenReturn(10) 358 whenever(mediaSubTitleWidgetState.alpha).thenReturn(1F) 359 360 // in current beizer, when the progress reach 0.38, the result will be 0.5 361 mediaViewController.squishViewState(mockViewState, 307.6F / 360F) 362 verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } 363 mediaViewController.squishViewState(mockViewState, 320F / 360F) 364 verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } 365 // media title and media subtitle are in same widget group, should be calculate together and 366 // have same alpha 367 mediaViewController.squishViewState(mockViewState, 353.8F / 360F) 368 verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } 369 verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } 370 mediaViewController.squishViewState(mockViewState, 360F / 360F) 371 verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } 372 verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } 373 } 374 375 @EnableSceneContainer 376 @Test 377 fun attachPlayer_seekBarDisabled_seekBarVisibilityIsSetToInvisible() { 378 mediaViewController.attachPlayer(viewHolder) 379 getEnabledChangeListener().onEnabledChanged(enabled = true) 380 getEnabledChangeListener().onEnabledChanged(enabled = false) 381 382 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar)) 383 .isEqualTo(ConstraintSet.INVISIBLE) 384 } 385 386 @EnableSceneContainer 387 @Test 388 fun attachPlayer_seekBarEnabled_seekBarVisible() { 389 mediaViewController.attachPlayer(viewHolder) 390 getEnabledChangeListener().onEnabledChanged(enabled = true) 391 392 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar)) 393 .isEqualTo(ConstraintSet.VISIBLE) 394 } 395 396 @EnableSceneContainer 397 @Test 398 fun attachPlayer_seekBarStatusUpdate_seekBarVisibilityChanges() { 399 mediaViewController.attachPlayer(viewHolder) 400 getEnabledChangeListener().onEnabledChanged(enabled = true) 401 402 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar)) 403 .isEqualTo(ConstraintSet.VISIBLE) 404 405 getEnabledChangeListener().onEnabledChanged(enabled = false) 406 407 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar)) 408 .isEqualTo(ConstraintSet.INVISIBLE) 409 } 410 411 @EnableSceneContainer 412 @Test 413 fun attachPlayer_notScrubbing_scrubbingViewsGone() { 414 mediaViewController.attachPlayer(viewHolder) 415 mediaViewController.canShowScrubbingTime = true 416 getScrubbingChangeListener().onScrubbingChanged(true) 417 getScrubbingChangeListener().onScrubbingChanged(false) 418 mainExecutor.runAllReady() 419 420 assertThat( 421 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) 422 ) 423 .isEqualTo(ConstraintSet.GONE) 424 assertThat( 425 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) 426 ) 427 .isEqualTo(ConstraintSet.GONE) 428 } 429 430 @EnableSceneContainer 431 @Test 432 fun setIsScrubbing_noSemanticActions_scrubbingViewsGone() { 433 mediaViewController.attachPlayer(viewHolder) 434 mediaViewController.canShowScrubbingTime = false 435 getScrubbingChangeListener().onScrubbingChanged(true) 436 mainExecutor.runAllReady() 437 438 assertThat( 439 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) 440 ) 441 .isEqualTo(ConstraintSet.GONE) 442 assertThat( 443 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) 444 ) 445 .isEqualTo(ConstraintSet.GONE) 446 } 447 448 @EnableSceneContainer 449 @Test 450 fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() { 451 mediaViewController.attachPlayer(viewHolder) 452 mediaViewController.setUpNextButtonInfo(true) 453 mediaViewController.setUpPrevButtonInfo(false) 454 getScrubbingChangeListener().onScrubbingChanged(true) 455 mainExecutor.runAllReady() 456 457 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext)) 458 .isEqualTo(ConstraintSet.VISIBLE) 459 assertThat( 460 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) 461 ) 462 .isEqualTo(ConstraintSet.GONE) 463 assertThat( 464 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) 465 ) 466 .isEqualTo(ConstraintSet.GONE) 467 } 468 469 @EnableSceneContainer 470 @Test 471 fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() { 472 mediaViewController.attachPlayer(viewHolder) 473 mediaViewController.setUpNextButtonInfo(false) 474 mediaViewController.setUpPrevButtonInfo(true) 475 getScrubbingChangeListener().onScrubbingChanged(true) 476 mainExecutor.runAllReady() 477 478 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev)) 479 .isEqualTo(ConstraintSet.VISIBLE) 480 assertThat( 481 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) 482 ) 483 .isEqualTo(ConstraintSet.GONE) 484 assertThat( 485 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) 486 ) 487 .isEqualTo(ConstraintSet.GONE) 488 } 489 490 @EnableSceneContainer 491 @Test 492 fun setIsScrubbing_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() { 493 mediaViewController.attachPlayer(viewHolder) 494 mediaViewController.setUpNextButtonInfo(true) 495 mediaViewController.setUpPrevButtonInfo(true) 496 mediaViewController.canShowScrubbingTime = true 497 getScrubbingChangeListener().onScrubbingChanged(true) 498 mainExecutor.runAllReady() 499 500 // Only in expanded, we should show the scrubbing times and hide prev+next 501 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev)) 502 .isEqualTo(ConstraintSet.GONE) 503 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext)) 504 .isEqualTo(ConstraintSet.GONE) 505 assertThat( 506 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) 507 ) 508 .isEqualTo(ConstraintSet.VISIBLE) 509 assertThat( 510 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) 511 ) 512 .isEqualTo(ConstraintSet.VISIBLE) 513 } 514 515 @EnableSceneContainer 516 @Test 517 fun setIsScrubbing_trueThenFalse_reservePrevAndNextButtons() { 518 mediaViewController.attachPlayer(viewHolder) 519 mediaViewController.setUpNextButtonInfo(true, ConstraintSet.INVISIBLE) 520 mediaViewController.setUpPrevButtonInfo(true, ConstraintSet.INVISIBLE) 521 mediaViewController.canShowScrubbingTime = true 522 523 getScrubbingChangeListener().onScrubbingChanged(true) 524 mainExecutor.runAllReady() 525 526 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev)) 527 .isEqualTo(ConstraintSet.INVISIBLE) 528 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext)) 529 .isEqualTo(ConstraintSet.INVISIBLE) 530 assertThat( 531 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) 532 ) 533 .isEqualTo(ConstraintSet.VISIBLE) 534 assertThat( 535 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) 536 ) 537 .isEqualTo(ConstraintSet.VISIBLE) 538 539 getScrubbingChangeListener().onScrubbingChanged(false) 540 mainExecutor.runAllReady() 541 542 // Only in expanded, we should hide the scrubbing times and show prev+next 543 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev)) 544 .isEqualTo(ConstraintSet.VISIBLE) 545 assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext)) 546 .isEqualTo(ConstraintSet.VISIBLE) 547 assertThat( 548 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time) 549 ) 550 .isEqualTo(ConstraintSet.GONE) 551 assertThat( 552 mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time) 553 ) 554 .isEqualTo(ConstraintSet.GONE) 555 } 556 557 private fun initGutsViewHolderMocks() { 558 settings = ImageButton(context) 559 cancel = View(context) 560 cancelText = TextView(context) 561 dismiss = FrameLayout(context) 562 dismissText = TextView(context) 563 whenever(gutsViewHolder.gutsText).thenReturn(gutsText) 564 whenever(gutsViewHolder.settings).thenReturn(settings) 565 whenever(gutsViewHolder.cancel).thenReturn(cancel) 566 whenever(gutsViewHolder.cancelText).thenReturn(cancelText) 567 whenever(gutsViewHolder.dismiss).thenReturn(dismiss) 568 whenever(gutsViewHolder.dismissText).thenReturn(dismissText) 569 } 570 571 private fun initMediaViewHolderMocks() { 572 titleText = TextView(context) 573 artistText = TextView(context) 574 explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator } 575 seamless = FrameLayout(context) 576 seamless.foreground = seamlessBackground 577 seamlessButton = View(context) 578 seamlessIcon = ImageView(context) 579 seamlessText = TextView(context) 580 seekBar = SeekBar(context).also { it.id = R.id.media_progress_bar } 581 582 actionPlayPause = ImageButton(context).also { it.id = R.id.actionPlayPause } 583 actionPrev = ImageButton(context).also { it.id = R.id.actionPrev } 584 actionNext = ImageButton(context).also { it.id = R.id.actionNext } 585 scrubbingElapsedTimeView = 586 TextView(context).also { it.id = R.id.media_scrubbing_elapsed_time } 587 scrubbingTotalTimeView = TextView(context).also { it.id = R.id.media_scrubbing_total_time } 588 589 multiRippleView = MultiRippleView(context, null) 590 turbulenceNoiseView = TurbulenceNoiseView(context, null) 591 loadingEffectView = LoadingEffectView(context, null) 592 593 whenever(viewHolder.player).thenReturn(view) 594 whenever(view.context).thenReturn(context) 595 whenever(viewHolder.albumView).thenReturn(albumView) 596 whenever(albumView.foreground).thenReturn(Mockito.mock(Drawable::class.java)) 597 whenever(viewHolder.titleText).thenReturn(titleText) 598 whenever(viewHolder.artistText).thenReturn(artistText) 599 whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator) 600 whenever(seamlessBackground.getDrawable(0)) 601 .thenReturn(Mockito.mock(GradientDrawable::class.java)) 602 whenever(viewHolder.seamless).thenReturn(seamless) 603 whenever(viewHolder.seamlessButton).thenReturn(seamlessButton) 604 whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon) 605 whenever(viewHolder.seamlessText).thenReturn(seamlessText) 606 whenever(viewHolder.seekBar).thenReturn(seekBar) 607 whenever(viewHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView) 608 whenever(viewHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView) 609 whenever(viewHolder.gutsViewHolder).thenReturn(gutsViewHolder) 610 whenever(seekBarViewModel.progress).thenReturn(seekBarData) 611 612 // Action buttons 613 whenever(viewHolder.actionPlayPause).thenReturn(actionPlayPause) 614 whenever(viewHolder.getAction(R.id.actionNext)).thenReturn(actionNext) 615 whenever(viewHolder.getAction(R.id.actionPrev)).thenReturn(actionPrev) 616 whenever(viewHolder.getAction(R.id.actionPlayPause)).thenReturn(actionPlayPause) 617 618 whenever(viewHolder.multiRippleView).thenReturn(multiRippleView) 619 whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView) 620 whenever(viewHolder.loadingEffectView).thenReturn(loadingEffectView) 621 } 622 623 private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener = 624 withArgCaptor { 625 verify(seekBarViewModel).setScrubbingChangeListener(capture()) 626 } 627 628 private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = withArgCaptor { 629 verify(seekBarViewModel).setEnabledChangeListener(capture()) 630 } 631 } 632