1 /*
<lambda>null2  * 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.controller
18 
19 import android.animation.Animator
20 import android.animation.AnimatorInflater
21 import android.animation.AnimatorSet
22 import android.content.Context
23 import android.content.res.Configuration
24 import android.graphics.Color
25 import android.graphics.Paint
26 import android.graphics.drawable.Drawable
27 import android.provider.Settings
28 import android.view.View
29 import android.view.animation.Interpolator
30 import androidx.annotation.VisibleForTesting
31 import androidx.constraintlayout.widget.ConstraintSet
32 import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT
33 import com.android.app.animation.Interpolators
34 import com.android.app.tracing.traceSection
35 import com.android.systemui.Flags
36 import com.android.systemui.dagger.qualifiers.Main
37 import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition
38 import com.android.systemui.media.controls.ui.animation.MetadataAnimationHandler
39 import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder
40 import com.android.systemui.media.controls.ui.binder.MediaRecommendationsViewBinder
41 import com.android.systemui.media.controls.ui.binder.SeekBarObserver
42 import com.android.systemui.media.controls.ui.controller.MediaCarouselController.Companion.calculateAlpha
43 import com.android.systemui.media.controls.ui.view.GutsViewHolder
44 import com.android.systemui.media.controls.ui.view.MediaHostState
45 import com.android.systemui.media.controls.ui.view.MediaViewHolder
46 import com.android.systemui.media.controls.ui.view.RecommendationViewHolder
47 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel
48 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
49 import com.android.systemui.media.controls.util.MediaFlags
50 import com.android.systemui.res.R
51 import com.android.systemui.statusbar.policy.ConfigurationController
52 import com.android.systemui.surfaceeffects.PaintDrawCallback
53 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect
54 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView
55 import com.android.systemui.surfaceeffects.ripple.MultiRippleController
56 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
57 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController
58 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
59 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
60 import com.android.systemui.util.animation.MeasurementInput
61 import com.android.systemui.util.animation.MeasurementOutput
62 import com.android.systemui.util.animation.TransitionLayout
63 import com.android.systemui.util.animation.TransitionLayoutController
64 import com.android.systemui.util.animation.TransitionViewState
65 import com.android.systemui.util.concurrency.DelayableExecutor
66 import com.android.systemui.util.settings.GlobalSettings
67 import java.lang.Float.max
68 import java.lang.Float.min
69 import java.util.Random
70 import javax.inject.Inject
71 
72 /**
73  * A class responsible for controlling a single instance of a media player handling interactions
74  * with the view instance and keeping the media view states up to date.
75  */
76 open class MediaViewController
77 @Inject
78 constructor(
79     private val context: Context,
80     private val configurationController: ConfigurationController,
81     private val mediaHostStatesManager: MediaHostStatesManager,
82     private val logger: MediaViewLogger,
83     private val seekBarViewModel: SeekBarViewModel,
84     @Main private val mainExecutor: DelayableExecutor,
85     private val mediaFlags: MediaFlags,
86     private val globalSettings: GlobalSettings,
87 ) {
88 
89     /**
90      * Indicating that the media view controller is for a notification-based player, session-based
91      * player, or recommendation
92      */
93     enum class TYPE {
94         PLAYER,
95         RECOMMENDATION
96     }
97 
98     companion object {
99         @JvmField val GUTS_ANIMATION_DURATION = 234L
100     }
101 
102     /** A listener when the current dimensions of the player change */
103     lateinit var sizeChangedListener: () -> Unit
104     lateinit var configurationChangeListener: () -> Unit
105     lateinit var recsConfigurationChangeListener: (MediaViewController, TransitionLayout) -> Unit
106     private var firstRefresh: Boolean = true
107     @VisibleForTesting private var transitionLayout: TransitionLayout? = null
108     private val layoutController = TransitionLayoutController()
109     private var animationDelay: Long = 0
110     private var animationDuration: Long = 0
111     private var animateNextStateChange: Boolean = false
112     private val measurement = MeasurementOutput(0, 0)
113     private var type: TYPE = TYPE.PLAYER
114 
115     /** A map containing all viewStates for all locations of this mediaState */
116     private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf()
117 
118     /**
119      * The ending location of the view where it ends when all animations and transitions have
120      * finished
121      */
122     @MediaLocation var currentEndLocation: Int = -1
123 
124     /** The starting location of the view where it starts for all animations and transitions */
125     @MediaLocation private var currentStartLocation: Int = -1
126 
127     /** The progress of the transition or 1.0 if there is no transition happening */
128     private var currentTransitionProgress: Float = 1.0f
129 
130     /** A temporary state used to store intermediate measurements. */
131     private val tmpState = TransitionViewState()
132 
133     /** A temporary state used to store intermediate measurements. */
134     private val tmpState2 = TransitionViewState()
135 
136     /** A temporary state used to store intermediate measurements. */
137     private val tmpState3 = TransitionViewState()
138 
139     /** A temporary cache key to be used to look up cache entries */
140     private val tmpKey = CacheKey()
141 
142     /**
143      * The current width of the player. This might not factor in case the player is animating to the
144      * current state, but represents the end state
145      */
146     var currentWidth: Int = 0
147     /**
148      * The current height of the player. This might not factor in case the player is animating to
149      * the current state, but represents the end state
150      */
151     var currentHeight: Int = 0
152 
153     /** Get the translationX of the layout */
154     var translationX: Float = 0.0f
155         private set
156         get() {
157             return transitionLayout?.translationX ?: 0.0f
158         }
159 
160     /** Get the translationY of the layout */
161     var translationY: Float = 0.0f
162         private set
163         get() {
164             return transitionLayout?.translationY ?: 0.0f
165         }
166 
167     /** Whether artwork is bound. */
168     var isArtworkBound: Boolean = false
169 
170     /** previous background artwork */
171     var prevArtwork: Drawable? = null
172 
173     /** Whether scrubbing time can show */
174     var canShowScrubbingTime: Boolean = false
175 
176     /** Whether user is touching the seek bar to change the position */
177     var isScrubbing: Boolean = false
178 
179     var isSeekBarEnabled: Boolean = false
180 
181     /** Not visible value for previous button when scrubbing */
182     private var prevNotVisibleValue = ConstraintSet.GONE
183     private var isPrevButtonAvailable = false
184 
185     /** Not visible value for next button when scrubbing */
186     private var nextNotVisibleValue = ConstraintSet.GONE
187     private var isNextButtonAvailable = false
188 
189     /** View holders for controller */
190     lateinit var recommendationViewHolder: RecommendationViewHolder
191     lateinit var mediaViewHolder: MediaViewHolder
192 
193     private lateinit var seekBarObserver: SeekBarObserver
194     private lateinit var turbulenceNoiseController: TurbulenceNoiseController
195     private lateinit var loadingEffect: LoadingEffect
196     private lateinit var turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig
197     private lateinit var noiseDrawCallback: PaintDrawCallback
198     private lateinit var stateChangedCallback: LoadingEffect.AnimationStateChangedCallback
199     internal lateinit var metadataAnimationHandler: MetadataAnimationHandler
200     internal lateinit var colorSchemeTransition: ColorSchemeTransition
201     internal lateinit var multiRippleController: MultiRippleController
202 
203     private val scrubbingChangeListener =
204         object : SeekBarViewModel.ScrubbingChangeListener {
205             override fun onScrubbingChanged(scrubbing: Boolean) {
206                 if (!mediaFlags.isSceneContainerEnabled()) return
207                 if (isScrubbing == scrubbing) return
208                 isScrubbing = scrubbing
209                 updateDisplayForScrubbingChange()
210             }
211         }
212 
213     private val enabledChangeListener =
214         object : SeekBarViewModel.EnabledChangeListener {
215             override fun onEnabledChanged(enabled: Boolean) {
216                 if (!mediaFlags.isSceneContainerEnabled()) return
217                 if (isSeekBarEnabled == enabled) return
218                 isSeekBarEnabled = enabled
219                 MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled)
220             }
221         }
222 
223     /**
224      * Sets the listening state of the player.
225      *
226      * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
227      * unnecessary work when the QS panel is closed.
228      *
229      * @param listening True when player should be active. Otherwise, false.
230      */
231     fun setListening(listening: Boolean) {
232         if (!mediaFlags.isSceneContainerEnabled()) return
233         seekBarViewModel.listening = listening
234     }
235 
236     /** A callback for config changes */
237     private val configurationListener =
238         object : ConfigurationController.ConfigurationListener {
239             var lastOrientation = -1
240 
241             override fun onConfigChanged(newConfig: Configuration?) {
242                 // Because the TransitionLayout is not always attached (and calculates/caches layout
243                 // results regardless of attach state), we have to force the layoutDirection of the
244                 // view
245                 // to the correct value for the user's current locale to ensure correct
246                 // recalculation
247                 // when/after calling refreshState()
248                 newConfig?.apply {
249                     if (transitionLayout?.rawLayoutDirection != layoutDirection) {
250                         transitionLayout?.layoutDirection = layoutDirection
251                         refreshState()
252                     }
253                     val newOrientation = newConfig.orientation
254                     if (lastOrientation != newOrientation) {
255                         // Layout dimensions are possibly changing, so we need to update them. (at
256                         // least on large screen devices)
257                         lastOrientation = newOrientation
258                         // Update the height of media controls for the expanded layout. it is needed
259                         // for large screen devices.
260                         setBackgroundHeights(
261                             context.resources.getDimensionPixelSize(
262                                 R.dimen.qs_media_session_height_expanded
263                             )
264                         )
265                     }
266                     if (mediaFlags.isSceneContainerEnabled()) {
267                         if (
268                             this@MediaViewController::recsConfigurationChangeListener.isInitialized
269                         ) {
270                             transitionLayout?.let {
271                                 recsConfigurationChangeListener.invoke(this@MediaViewController, it)
272                             }
273                         }
274                     } else if (
275                         this@MediaViewController::configurationChangeListener.isInitialized
276                     ) {
277                         configurationChangeListener.invoke()
278                         refreshState()
279                     }
280                 }
281             }
282         }
283 
284     /** A callback for media state changes */
285     val stateCallback =
286         object : MediaHostStatesManager.Callback {
287             override fun onHostStateChanged(
288                 @MediaLocation location: Int,
289                 mediaHostState: MediaHostState
290             ) {
291                 if (location == currentEndLocation || location == currentStartLocation) {
292                     setCurrentState(
293                         currentStartLocation,
294                         currentEndLocation,
295                         currentTransitionProgress,
296                         applyImmediately = false
297                     )
298                 }
299             }
300         }
301 
302     /**
303      * The expanded constraint set used to render a expanded player. If it is modified, make sure to
304      * call [refreshState]
305      */
306     var collapsedLayout = ConstraintSet()
307         @VisibleForTesting set
308 
309     /**
310      * The expanded constraint set used to render a collapsed player. If it is modified, make sure
311      * to call [refreshState]
312      */
313     var expandedLayout = ConstraintSet()
314         @VisibleForTesting set
315 
316     /** Whether the guts are visible for the associated player. */
317     var isGutsVisible = false
318         private set
319 
320     /** Size provided by the scene framework container */
321     var widthInSceneContainerPx = 0
322     var heightInSceneContainerPx = 0
323 
324     init {
325         mediaHostStatesManager.addController(this)
326         layoutController.sizeChangedListener = { width: Int, height: Int ->
327             currentWidth = width
328             currentHeight = height
329             sizeChangedListener.invoke()
330         }
331         configurationController.addCallback(configurationListener)
332     }
333 
334     /**
335      * Notify this controller that the view has been removed and all listeners should be destroyed
336      */
337     fun onDestroy() {
338         if (mediaFlags.isSceneContainerEnabled()) {
339             if (this::seekBarObserver.isInitialized) {
340                 seekBarViewModel.progress.removeObserver(seekBarObserver)
341             }
342             seekBarViewModel.removeScrubbingChangeListener(scrubbingChangeListener)
343             seekBarViewModel.removeEnabledChangeListener(enabledChangeListener)
344             seekBarViewModel.onDestroy()
345         }
346         mediaHostStatesManager.removeController(this)
347         configurationController.removeCallback(configurationListener)
348     }
349 
350     /** Show guts with an animated transition. */
351     fun openGuts() {
352         if (isGutsVisible) return
353         isGutsVisible = true
354         animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
355         setCurrentState(
356             currentStartLocation,
357             currentEndLocation,
358             currentTransitionProgress,
359             applyImmediately = false,
360             isGutsAnimation = true,
361         )
362     }
363 
364     /**
365      * Close the guts for the associated player.
366      *
367      * @param immediate if `false`, it will animate the transition.
368      */
369     @JvmOverloads
370     fun closeGuts(immediate: Boolean = false) {
371         if (!isGutsVisible) return
372         isGutsVisible = false
373         if (!immediate) {
374             animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
375         }
376         setCurrentState(
377             currentStartLocation,
378             currentEndLocation,
379             currentTransitionProgress,
380             applyImmediately = immediate,
381             isGutsAnimation = true,
382         )
383     }
384 
385     private fun ensureAllMeasurements() {
386         val mediaStates = mediaHostStatesManager.mediaHostStates
387         for (entry in mediaStates) {
388             obtainViewState(entry.value)
389         }
390     }
391 
392     /** Get the constraintSet for a given expansion */
393     private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
394         if (expansion > 0) expandedLayout else collapsedLayout
395 
396     /** Set the height of UMO background constraints. */
397     private fun setBackgroundHeights(height: Int) {
398         val backgroundIds =
399             if (type == TYPE.PLAYER) {
400                 MediaViewHolder.backgroundIds
401             } else {
402                 setOf(RecommendationViewHolder.backgroundId)
403             }
404         backgroundIds.forEach { id -> expandedLayout.getConstraint(id).layout.mHeight = height }
405     }
406 
407     /**
408      * Set the views to be showing/hidden based on the [isGutsVisible] for a given
409      * [TransitionViewState].
410      */
411     private fun setGutsViewState(viewState: TransitionViewState) {
412         val controlsIds =
413             when (type) {
414                 TYPE.PLAYER -> MediaViewHolder.controlsIds
415                 TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds
416             }
417         val gutsIds = GutsViewHolder.ids
418         controlsIds.forEach { id ->
419             viewState.widgetStates.get(id)?.let { state ->
420                 // Make sure to use the unmodified state if guts are not visible.
421                 state.alpha = if (isGutsVisible) 0f else state.alpha
422                 state.gone = if (isGutsVisible) true else state.gone
423             }
424         }
425         gutsIds.forEach { id ->
426             viewState.widgetStates.get(id)?.let { state ->
427                 // Make sure to use the unmodified state if guts are visible
428                 state.alpha = if (isGutsVisible) state.alpha else 0f
429                 state.gone = if (isGutsVisible) state.gone else true
430             }
431         }
432     }
433 
434     /** Apply squishFraction to a copy of viewState such that the cached version is untouched. */
435     internal fun squishViewState(
436         viewState: TransitionViewState,
437         squishFraction: Float
438     ): TransitionViewState {
439         val squishedViewState = viewState.copy()
440         val squishedHeight = (squishedViewState.measureHeight * squishFraction).toInt()
441         squishedViewState.height = squishedHeight
442         // We are not overriding the squishedViewStates height but only the children to avoid
443         // them remeasuring the whole view. Instead it just remains as the original size
444         MediaViewHolder.backgroundIds.forEach { id ->
445             squishedViewState.widgetStates.get(id)?.let { state -> state.height = squishedHeight }
446         }
447 
448         // media player
449         calculateWidgetGroupAlphaForSquishiness(
450             MediaViewHolder.expandedBottomActionIds,
451             squishedViewState.measureHeight.toFloat(),
452             squishedViewState,
453             squishFraction
454         )
455         calculateWidgetGroupAlphaForSquishiness(
456             MediaViewHolder.detailIds,
457             squishedViewState.measureHeight.toFloat(),
458             squishedViewState,
459             squishFraction
460         )
461         // recommendation card
462         val titlesTop =
463             calculateWidgetGroupAlphaForSquishiness(
464                 RecommendationViewHolder.mediaTitlesAndSubtitlesIds,
465                 squishedViewState.measureHeight.toFloat(),
466                 squishedViewState,
467                 squishFraction
468             )
469         calculateWidgetGroupAlphaForSquishiness(
470             RecommendationViewHolder.mediaContainersIds,
471             titlesTop,
472             squishedViewState,
473             squishFraction
474         )
475         return squishedViewState
476     }
477 
478     /**
479      * This function is to make each widget in UMO disappear before being clipped by squished UMO
480      *
481      * The general rule is that widgets in UMO has been divided into several groups, and widgets in
482      * one group have the same alpha during squishing It will change from alpha 0.0 when the visible
483      * bottom of UMO reach the bottom of this group It will change to alpha 1.0 when the visible
484      * bottom of UMO reach the top of the group below e.g.Album title, artist title and play-pause
485      * button will change alpha together.
486      *
487      * ```
488      *     And their alpha becomes 1.0 when the visible bottom of UMO reach the top of controls,
489      *     including progress bar, next button, previous button
490      * ```
491      *
492      * widgetGroupIds: a group of widgets have same state during UMO is squished,
493      * ```
494      *     e.g. Album title, artist title and play-pause button
495      * ```
496      *
497      * groupEndPosition: the height of UMO, when the height reaches this value,
498      * ```
499      *     widgets in this group should have 1.0 as alpha
500      *     e.g., the group of album title, artist title and play-pause button will become fully
501      *         visible when the height of UMO reaches the top of controls group
502      *         (progress bar, previous button and next button)
503      * ```
504      *
505      * squishedViewState: hold the widgetState of each widget, which will be modified
506      * squishFraction: the squishFraction of UMO
507      */
508     private fun calculateWidgetGroupAlphaForSquishiness(
509         widgetGroupIds: Set<Int>,
510         groupEndPosition: Float,
511         squishedViewState: TransitionViewState,
512         squishFraction: Float
513     ): Float {
514         val nonsquishedHeight = squishedViewState.measureHeight
515         var groupTop = squishedViewState.measureHeight.toFloat()
516         var groupBottom = 0F
517         widgetGroupIds.forEach { id ->
518             squishedViewState.widgetStates.get(id)?.let { state ->
519                 groupTop = min(groupTop, state.y)
520                 groupBottom = max(groupBottom, state.y + state.height)
521             }
522         }
523         // startPosition means to the height of squished UMO where the widget alpha should start
524         // changing from 0.0
525         // generally, it equals to the bottom of widgets, so that we can meet the requirement that
526         // widget should not go beyond the bounds of background
527         // endPosition means to the height of squished UMO where the widget alpha should finish
528         // changing alpha to 1.0
529         var startPosition = groupBottom
530         val endPosition = groupEndPosition
531         if (startPosition == endPosition) {
532             startPosition = (endPosition - 0.2 * (groupBottom - groupTop)).toFloat()
533         }
534         widgetGroupIds.forEach { id ->
535             squishedViewState.widgetStates.get(id)?.let { state ->
536                 // Don't modify alpha for elements that should be invisible (e.g. disabled seekbar)
537                 if (state.alpha != 0f) {
538                     state.alpha =
539                         calculateAlpha(
540                             squishFraction,
541                             startPosition / nonsquishedHeight,
542                             endPosition / nonsquishedHeight
543                         )
544                 }
545             }
546         }
547         return groupTop // used for the widget group above this group
548     }
549 
550     /**
551      * Obtain a new viewState for a given media state. This usually returns a cached state, but if
552      * it's not available, it will recreate one by measuring, which may be expensive.
553      */
554     @VisibleForTesting
555     fun obtainViewState(
556         state: MediaHostState?,
557         isGutsAnimation: Boolean = false
558     ): TransitionViewState? {
559         if (mediaFlags.isSceneContainerEnabled()) {
560             return obtainSceneContainerViewState()
561         }
562 
563         if (state == null || state.measurementInput == null) {
564             return null
565         }
566         // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
567         var cacheKey = getKey(state, isGutsVisible, tmpKey)
568         val viewState = viewStates[cacheKey]
569         if (viewState != null) {
570             // we already have cached this measurement, let's continue
571             if (state.squishFraction <= 1f && !isGutsAnimation) {
572                 return squishViewState(viewState, state.squishFraction)
573             }
574             return viewState
575         }
576         // Copy the key since this might call recursively into it and we're using tmpKey
577         cacheKey = cacheKey.copy()
578         val result: TransitionViewState?
579 
580         if (transitionLayout == null) {
581             return null
582         }
583         // Let's create a new measurement
584         if (state.expansion == 0.0f || state.expansion == 1.0f) {
585             if (state.expansion == 1.0f) {
586                 val height =
587                     if (state.expandedMatchesParentHeight) {
588                         MATCH_CONSTRAINT
589                     } else {
590                         context.resources.getDimensionPixelSize(
591                             R.dimen.qs_media_session_height_expanded
592                         )
593                     }
594                 setBackgroundHeights(height)
595             }
596 
597             result =
598                 transitionLayout!!.calculateViewState(
599                     state.measurementInput!!,
600                     constraintSetForExpansion(state.expansion),
601                     TransitionViewState()
602                 )
603 
604             setGutsViewState(result)
605             // We don't want to cache interpolated or null states as this could quickly fill up
606             // our cache. We only cache the start and the end states since the interpolation
607             // is cheap
608             viewStates[cacheKey] = result
609         } else {
610             // This is an interpolated state
611             val startState = state.copy().also { it.expansion = 0.0f }
612 
613             // Given that we have a measurement and a view, let's get (guaranteed) viewstates
614             // from the start and end state and interpolate them
615             val startViewState = obtainViewState(startState, isGutsAnimation) as TransitionViewState
616             val endState = state.copy().also { it.expansion = 1.0f }
617             val endViewState = obtainViewState(endState, isGutsAnimation) as TransitionViewState
618             result =
619                 layoutController.getInterpolatedState(startViewState, endViewState, state.expansion)
620         }
621         // Skip the adjustments of squish view state if UMO changes due to guts animation.
622         if (state.squishFraction <= 1f && !isGutsAnimation) {
623             return squishViewState(result, state.squishFraction)
624         }
625         return result
626     }
627 
628     private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey {
629         result.apply {
630             heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
631             widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
632             expansion = state.expansion
633             gutsVisible = guts
634         }
635         return result
636     }
637 
638     /**
639      * Attach a view to this controller. This may perform measurements if it's not available yet and
640      * should therefore be done carefully.
641      */
642     fun attach(transitionLayout: TransitionLayout, type: TYPE) =
643         traceSection("MediaViewController#attach") {
644             loadLayoutForType(type)
645             logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation)
646             this.transitionLayout = transitionLayout
647             layoutController.attach(transitionLayout)
648             if (currentEndLocation == -1) {
649                 return
650             }
651             // Set the previously set state immediately to the view, now that it's finally attached
652             setCurrentState(
653                 startLocation = currentStartLocation,
654                 endLocation = currentEndLocation,
655                 transitionProgress = currentTransitionProgress,
656                 applyImmediately = true
657             )
658         }
659 
660     fun attachPlayer(mediaViewHolder: MediaViewHolder) {
661         if (!mediaFlags.isSceneContainerEnabled()) return
662         this.mediaViewHolder = mediaViewHolder
663 
664         // Setting up seek bar.
665         seekBarObserver = SeekBarObserver(mediaViewHolder)
666         seekBarViewModel.progress.observeForever(seekBarObserver)
667         seekBarViewModel.attachTouchHandlers(mediaViewHolder.seekBar)
668         seekBarViewModel.setScrubbingChangeListener(scrubbingChangeListener)
669         seekBarViewModel.setEnabledChangeListener(enabledChangeListener)
670 
671         val mediaCard = mediaViewHolder.player
672         attach(mediaViewHolder.player, TYPE.PLAYER)
673 
674         val turbulenceNoiseView = mediaViewHolder.turbulenceNoiseView
675         turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView)
676 
677         multiRippleController = MultiRippleController(mediaViewHolder.multiRippleView)
678 
679         // Metadata Animation
680         val titleText = mediaViewHolder.titleText
681         val artistText = mediaViewHolder.artistText
682         val explicitIndicator = mediaViewHolder.explicitIndicator
683         val enter =
684             loadAnimator(
685                 mediaCard.context,
686                 R.anim.media_metadata_enter,
687                 Interpolators.EMPHASIZED_DECELERATE,
688                 titleText,
689                 artistText,
690                 explicitIndicator
691             )
692         val exit =
693             loadAnimator(
694                 mediaCard.context,
695                 R.anim.media_metadata_exit,
696                 Interpolators.EMPHASIZED_ACCELERATE,
697                 titleText,
698                 artistText,
699                 explicitIndicator
700             )
701         metadataAnimationHandler = MetadataAnimationHandler(exit, enter)
702 
703         colorSchemeTransition =
704             ColorSchemeTransition(
705                 mediaCard.context,
706                 mediaViewHolder,
707                 multiRippleController,
708                 turbulenceNoiseController
709             )
710 
711         // For Turbulence noise.
712         val loadingEffectView = mediaViewHolder.loadingEffectView
713         noiseDrawCallback =
714             object : PaintDrawCallback {
715                 override fun onDraw(paint: Paint) {
716                     loadingEffectView.draw(paint)
717                 }
718             }
719         stateChangedCallback =
720             object : LoadingEffect.AnimationStateChangedCallback {
721                 override fun onStateChanged(
722                     oldState: LoadingEffect.AnimationState,
723                     newState: LoadingEffect.AnimationState
724                 ) {
725                     if (newState === LoadingEffect.AnimationState.NOT_PLAYING) {
726                         loadingEffectView.visibility = View.INVISIBLE
727                     } else {
728                         loadingEffectView.visibility = View.VISIBLE
729                     }
730                 }
731             }
732     }
733 
734     fun updateAnimatorDurationScale() {
735         if (!mediaFlags.isSceneContainerEnabled()) return
736         if (this::seekBarObserver.isInitialized) {
737             seekBarObserver.animationEnabled =
738                 globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f
739         }
740     }
741 
742     /** update view with the needed UI changes when user touches seekbar. */
743     private fun updateDisplayForScrubbingChange() {
744         mainExecutor.execute {
745             val isTimeVisible = canShowScrubbingTime && isScrubbing
746             MediaControlViewBinder.setVisibleAndAlpha(
747                 expandedLayout,
748                 mediaViewHolder.scrubbingTotalTimeView.id,
749                 isTimeVisible
750             )
751             MediaControlViewBinder.setVisibleAndAlpha(
752                 expandedLayout,
753                 mediaViewHolder.scrubbingElapsedTimeView.id,
754                 isTimeVisible
755             )
756 
757             MediaControlViewModel.SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach { id ->
758                 val isButtonVisible: Boolean
759                 val notVisibleValue: Int
760                 when (id) {
761                     R.id.actionPrev -> {
762                         isButtonVisible = isPrevButtonAvailable && !isTimeVisible
763                         notVisibleValue = prevNotVisibleValue
764                     }
765                     R.id.actionNext -> {
766                         isButtonVisible = isNextButtonAvailable && !isTimeVisible
767                         notVisibleValue = nextNotVisibleValue
768                     }
769                     else -> {
770                         isButtonVisible = !isTimeVisible
771                         notVisibleValue = ConstraintSet.GONE
772                     }
773                 }
774                 MediaControlViewBinder.setSemanticButtonVisibleAndAlpha(
775                     mediaViewHolder.getAction(id),
776                     expandedLayout,
777                     collapsedLayout,
778                     isButtonVisible,
779                     notVisibleValue,
780                     showInCollapsed = true
781                 )
782             }
783 
784             if (!metadataAnimationHandler.isRunning) {
785                 refreshState()
786             }
787         }
788     }
789 
790     fun attachRecommendations(recommendationViewHolder: RecommendationViewHolder) {
791         if (!mediaFlags.isSceneContainerEnabled()) return
792         this.recommendationViewHolder = recommendationViewHolder
793 
794         attach(recommendationViewHolder.recommendations, TYPE.RECOMMENDATION)
795         recsConfigurationChangeListener =
796             MediaRecommendationsViewBinder::updateRecommendationsVisibility
797     }
798 
799     fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) {
800         if (!mediaFlags.isSceneContainerEnabled()) return
801         seekBarViewModel.logSeek = onSeek
802         onBindSeekBar.invoke(seekBarViewModel)
803     }
804 
805     fun setUpTurbulenceNoise() {
806         if (!mediaFlags.isSceneContainerEnabled()) return
807         if (!this::turbulenceNoiseAnimationConfig.isInitialized) {
808             turbulenceNoiseAnimationConfig =
809                 createTurbulenceNoiseConfig(
810                     mediaViewHolder.loadingEffectView,
811                     mediaViewHolder.turbulenceNoiseView,
812                     colorSchemeTransition
813                 )
814         }
815         if (Flags.shaderlibLoadingEffectRefactor()) {
816             if (!this::loadingEffect.isInitialized) {
817                 loadingEffect =
818                     LoadingEffect(
819                         TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
820                         turbulenceNoiseAnimationConfig,
821                         noiseDrawCallback,
822                         stateChangedCallback
823                     )
824             }
825             colorSchemeTransition.loadingEffect = loadingEffect
826             loadingEffect.play()
827             mainExecutor.executeDelayed(
828                 loadingEffect::finish,
829                 MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION
830             )
831         } else {
832             turbulenceNoiseController.play(
833                 TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
834                 turbulenceNoiseAnimationConfig
835             )
836             mainExecutor.executeDelayed(
837                 turbulenceNoiseController::finish,
838                 MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION
839             )
840         }
841     }
842 
843     /**
844      * Obtain a measurement for a given location. This makes sure that the state is up to date and
845      * all widgets know their location. Calling this method may create a measurement if we don't
846      * have a cached value available already.
847      */
848     fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? =
849         traceSection("MediaViewController#getMeasurementsForState") {
850             // measurements should never factor in the squish fraction
851             val viewState = obtainViewState(hostState) ?: return null
852             measurement.measuredWidth = viewState.measureWidth
853             measurement.measuredHeight = viewState.measureHeight
854             return measurement
855         }
856 
857     /**
858      * Set a new state for the controlled view which can be an interpolation between multiple
859      * locations.
860      */
861     fun setCurrentState(
862         @MediaLocation startLocation: Int,
863         @MediaLocation endLocation: Int,
864         transitionProgress: Float,
865         applyImmediately: Boolean,
866         isGutsAnimation: Boolean = false,
867     ) =
868         traceSection("MediaViewController#setCurrentState") {
869             currentEndLocation = endLocation
870             currentStartLocation = startLocation
871             currentTransitionProgress = transitionProgress
872             logger.logMediaLocation("setCurrentState", startLocation, endLocation)
873 
874             val shouldAnimate = animateNextStateChange && !applyImmediately
875 
876             val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
877             val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
878 
879             // Obtain the view state that we'd want to be at the end
880             // The view might not be bound yet or has never been measured and in that case will be
881             // reset once the state is fully available
882             var endViewState = obtainViewState(endHostState, isGutsAnimation) ?: return
883             endViewState = updateViewStateSize(endViewState, endLocation, tmpState2)!!
884             layoutController.setMeasureState(endViewState)
885 
886             // If the view isn't bound, we can drop the animation, otherwise we'll execute it
887             animateNextStateChange = false
888             if (transitionLayout == null) {
889                 return
890             }
891 
892             val result: TransitionViewState
893             var startViewState = obtainViewState(startHostState, isGutsAnimation)
894             startViewState = updateViewStateSize(startViewState, startLocation, tmpState3)
895 
896             if (!endHostState.visible) {
897                 // Let's handle the case where the end is gone first. In this case we take the
898                 // start viewState and will make it gone
899                 if (startViewState == null || startHostState == null || !startHostState.visible) {
900                     // the start isn't a valid state, let's use the endstate directly
901                     result = endViewState
902                 } else {
903                     // Let's get the gone presentation from the start state
904                     result =
905                         layoutController.getGoneState(
906                             startViewState,
907                             startHostState.disappearParameters,
908                             transitionProgress,
909                             tmpState
910                         )
911                 }
912             } else if (startHostState != null && !startHostState.visible) {
913                 // We have a start state and it is gone.
914                 // Let's get presentation from the endState
915                 result =
916                     layoutController.getGoneState(
917                         endViewState,
918                         endHostState.disappearParameters,
919                         1.0f - transitionProgress,
920                         tmpState
921                     )
922             } else if (transitionProgress == 1.0f || startViewState == null) {
923                 // We're at the end. Let's use that state
924                 result = endViewState
925             } else if (transitionProgress == 0.0f) {
926                 // We're at the start. Let's use that state
927                 result = startViewState
928             } else {
929                 result =
930                     layoutController.getInterpolatedState(
931                         startViewState,
932                         endViewState,
933                         transitionProgress,
934                         tmpState
935                     )
936             }
937             logger.logMediaSize(
938                 "setCurrentState (progress $transitionProgress)",
939                 result.width,
940                 result.height
941             )
942             layoutController.setState(
943                 result,
944                 applyImmediately,
945                 shouldAnimate,
946                 animationDuration,
947                 animationDelay,
948                 isGutsAnimation,
949             )
950         }
951 
952     private fun updateViewStateSize(
953         viewState: TransitionViewState?,
954         location: Int,
955         outState: TransitionViewState
956     ): TransitionViewState? {
957         var result = viewState?.copy(outState) ?: return null
958         val state = mediaHostStatesManager.mediaHostStates[location]
959         val overrideSize = mediaHostStatesManager.carouselSizes[location]
960         var overridden = false
961         overrideSize?.let {
962             // To be safe we're using a maximum here. The override size should always be set
963             // properly though.
964             if (
965                 result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth
966             ) {
967                 result.measureHeight = Math.max(it.measuredHeight, result.measureHeight)
968                 result.measureWidth = Math.max(it.measuredWidth, result.measureWidth)
969                 // The measureHeight and the shown height should both be set to the overridden
970                 // height
971                 result.height = result.measureHeight
972                 result.width = result.measureWidth
973                 // Make sure all background views are also resized such that their size is correct
974                 MediaViewHolder.backgroundIds.forEach { id ->
975                     result.widgetStates.get(id)?.let { state ->
976                         state.height = result.height
977                         state.width = result.width
978                     }
979                 }
980                 overridden = true
981             }
982         }
983         if (overridden && state != null && state.squishFraction <= 1f) {
984             // Let's squish the media player if our size was overridden
985             result = squishViewState(result, state.squishFraction)
986         }
987         logger.logMediaSize("update to carousel", result.width, result.height)
988         return result
989     }
990 
991     private fun loadLayoutForType(type: TYPE) {
992         this.type = type
993 
994         // These XML resources contain ConstraintSets that will apply to this player type's layout
995         when (type) {
996             TYPE.PLAYER -> {
997                 collapsedLayout.load(context, R.xml.media_session_collapsed)
998                 expandedLayout.load(context, R.xml.media_session_expanded)
999             }
1000             TYPE.RECOMMENDATION -> {
1001                 collapsedLayout.load(context, R.xml.media_recommendations_collapsed)
1002                 expandedLayout.load(context, R.xml.media_recommendations_expanded)
1003             }
1004         }
1005         refreshState()
1006     }
1007 
1008     /** Get a view state based on the width and height set by the scene */
1009     private fun obtainSceneContainerViewState(): TransitionViewState? {
1010         logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx)
1011 
1012         // Similar to obtainViewState: Let's create a new measurement
1013         val result =
1014             transitionLayout?.calculateViewState(
1015                 MeasurementInput(widthInSceneContainerPx, heightInSceneContainerPx),
1016                 expandedLayout,
1017                 TransitionViewState()
1018             )
1019         result?.let {
1020             // And then ensure the guts visibility is set correctly
1021             setGutsViewState(it)
1022         }
1023         return result
1024     }
1025 
1026     /**
1027      * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event
1028      * of [location] not being visible, [locationWhenHidden] will be used instead.
1029      *
1030      * @param location Target
1031      * @param locationWhenHidden Location that will be used when the target is not
1032      *   [MediaHost.visible]
1033      * @return State require for executing a transition, and also the respective [MediaHost].
1034      */
1035     private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? {
1036         val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null
1037         if (mediaFlags.isSceneContainerEnabled()) {
1038             return obtainSceneContainerViewState()
1039         }
1040 
1041         val viewState = obtainViewState(mediaHostState)
1042         if (viewState != null) {
1043             // update the size of the viewstate for the location with the override
1044             updateViewStateSize(viewState, location, tmpState)
1045             return tmpState
1046         }
1047         return viewState
1048     }
1049 
1050     /**
1051      * Notify that the location is changing right now and a [setCurrentState] change is imminent.
1052      * This updates the width the view will me measured with.
1053      */
1054     fun onLocationPreChange(@MediaLocation newLocation: Int) {
1055         obtainViewStateForLocation(newLocation)?.let { layoutController.setMeasureState(it) }
1056     }
1057 
1058     /** Request that the next state change should be animated with the given parameters. */
1059     fun animatePendingStateChange(duration: Long, delay: Long) {
1060         animateNextStateChange = true
1061         animationDuration = duration
1062         animationDelay = delay
1063     }
1064 
1065     /** Clear all existing measurements and refresh the state to match the view. */
1066     fun refreshState() =
1067         traceSection("MediaViewController#refreshState") {
1068             if (mediaFlags.isSceneContainerEnabled()) {
1069                 // We don't need to recreate measurements for scene container, since it's a known
1070                 // size. Just get the view state and update the layout controller
1071                 obtainSceneContainerViewState()?.let {
1072                     // Get scene container state, then setCurrentState
1073                     layoutController.setState(
1074                         state = it,
1075                         applyImmediately = true,
1076                         animate = false,
1077                         isGuts = false,
1078                     )
1079                 }
1080                 return
1081             }
1082 
1083             // Let's clear all of our measurements and recreate them!
1084             viewStates.clear()
1085             if (firstRefresh) {
1086                 // This is the first bind, let's ensure we pre-cache all measurements. Otherwise
1087                 // We'll just load these on demand.
1088                 ensureAllMeasurements()
1089                 firstRefresh = false
1090             }
1091             setCurrentState(
1092                 currentStartLocation,
1093                 currentEndLocation,
1094                 currentTransitionProgress,
1095                 applyImmediately = true
1096             )
1097         }
1098 
1099     @VisibleForTesting
1100     protected open fun loadAnimator(
1101         context: Context,
1102         animId: Int,
1103         motionInterpolator: Interpolator?,
1104         vararg targets: View?
1105     ): AnimatorSet {
1106         val animators = ArrayList<Animator>()
1107         for (target in targets) {
1108             val animator = AnimatorInflater.loadAnimator(context, animId) as AnimatorSet
1109             animator.childAnimations[0].interpolator = motionInterpolator
1110             animator.setTarget(target)
1111             animators.add(animator)
1112         }
1113         val result = AnimatorSet()
1114         result.playTogether(animators)
1115         return result
1116     }
1117 
1118     private fun createTurbulenceNoiseConfig(
1119         loadingEffectView: LoadingEffectView,
1120         turbulenceNoiseView: TurbulenceNoiseView,
1121         colorSchemeTransition: ColorSchemeTransition
1122     ): TurbulenceNoiseAnimationConfig {
1123         val targetView: View =
1124             if (Flags.shaderlibLoadingEffectRefactor()) {
1125                 loadingEffectView
1126             } else {
1127                 turbulenceNoiseView
1128             }
1129         val width = targetView.width
1130         val height = targetView.height
1131         val random = Random()
1132         return TurbulenceNoiseAnimationConfig(
1133             gridCount = 2.14f,
1134             TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER,
1135             random.nextFloat(),
1136             random.nextFloat(),
1137             random.nextFloat(),
1138             noiseMoveSpeedX = 0.42f,
1139             noiseMoveSpeedY = 0f,
1140             TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z,
1141             // Color will be correctly updated in ColorSchemeTransition.
1142             colorSchemeTransition.accentPrimary.currentColor,
1143             screenColor = Color.BLACK,
1144             width.toFloat(),
1145             height.toFloat(),
1146             TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS,
1147             easeInDuration = 1350f,
1148             easeOutDuration = 1350f,
1149             targetView.context.resources.displayMetrics.density,
1150             lumaMatteBlendFactor = 0.26f,
1151             lumaMatteOverallBrightness = 0.09f,
1152             shouldInverseNoiseLuminosity = false
1153         )
1154     }
1155 
1156     fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
1157         if (!mediaFlags.isSceneContainerEnabled()) return
1158         isPrevButtonAvailable = isAvailable
1159         prevNotVisibleValue = notVisibleValue
1160     }
1161 
1162     fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
1163         if (!mediaFlags.isSceneContainerEnabled()) return
1164         isNextButtonAvailable = isAvailable
1165         nextNotVisibleValue = notVisibleValue
1166     }
1167 }
1168 
1169 /** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */
1170 private data class CacheKey(
1171     var widthMeasureSpec: Int = -1,
1172     var heightMeasureSpec: Int = -1,
1173     var expansion: Float = 0.0f,
1174     var gutsVisible: Boolean = false
1175 )
1176