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