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