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