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.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.annotation.IntDef
23 import android.content.Context
24 import android.content.res.Configuration
25 import android.database.ContentObserver
26 import android.graphics.Rect
27 import android.net.Uri
28 import android.os.Handler
29 import android.os.UserHandle
30 import android.provider.Settings
31 import android.util.MathUtils
32 import android.view.View
33 import android.view.ViewGroup
34 import android.view.ViewGroupOverlay
35 import androidx.annotation.VisibleForTesting
36 import com.android.app.animation.Interpolators
37 import com.android.app.tracing.traceSection
38 import com.android.keyguard.KeyguardViewController
39 import com.android.systemui.Flags.mediaControlsLockscreenShadeBugFix
40 import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel
41 import com.android.systemui.dagger.SysUISingleton
42 import com.android.systemui.dagger.qualifiers.Application
43 import com.android.systemui.dagger.qualifiers.Main
44 import com.android.systemui.dreams.DreamOverlayStateController
45 import com.android.systemui.keyguard.WakefulnessLifecycle
46 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
47 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
48 import com.android.systemui.media.controls.ui.view.MediaHost
49 import com.android.systemui.media.dream.MediaDreamComplication
50 import com.android.systemui.plugins.statusbar.StatusBarStateController
51 import com.android.systemui.res.R
52 import com.android.systemui.scene.shared.flag.SceneContainerFlag
53 import com.android.systemui.shade.domain.interactor.ShadeInteractor
54 import com.android.systemui.statusbar.CrossFadeHelper
55 import com.android.systemui.statusbar.StatusBarState
56 import com.android.systemui.statusbar.SysuiStatusBarStateController
57 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
58 import com.android.systemui.statusbar.phone.KeyguardBypassController
59 import com.android.systemui.statusbar.policy.ConfigurationController
60 import com.android.systemui.statusbar.policy.KeyguardStateController
61 import com.android.systemui.statusbar.policy.SplitShadeStateController
62 import com.android.systemui.util.animation.UniqueObjectHostView
63 import com.android.systemui.util.settings.SecureSettings
64 import javax.inject.Inject
65 import kotlinx.coroutines.CoroutineScope
66 import kotlinx.coroutines.ExperimentalCoroutinesApi
67 import kotlinx.coroutines.flow.collectLatest
68 import kotlinx.coroutines.flow.combine
69 import kotlinx.coroutines.flow.distinctUntilChanged
70 import kotlinx.coroutines.flow.mapLatest
71 import com.android.app.tracing.coroutines.launchTraced as launch
72 
73 private val TAG: String = MediaHierarchyManager::class.java.simpleName
74 
75 /** Similarly to isShown but also excludes views that have 0 alpha */
76 val View.isShownNotFaded: Boolean
77     get() {
78         var current: View = this
79         while (true) {
80             if (current.visibility != View.VISIBLE) {
81                 return false
82             }
83             if (current.alpha == 0.0f) {
84                 return false
85             }
86             val parent = current.parent ?: return false // We are not attached to the view root
87             if (parent !is View) {
88                 // we reached the viewroot, hurray
89                 return true
90             }
91             current = parent
92         }
93     }
94 
95 /**
96  * This manager is responsible for placement of the unique media view between the different hosts
97  * and animate the positions of the views to achieve seamless transitions.
98  */
99 @OptIn(ExperimentalCoroutinesApi::class)
100 @SysUISingleton
101 class MediaHierarchyManager
102 @Inject
103 constructor(
104     private val context: Context,
105     private val statusBarStateController: SysuiStatusBarStateController,
106     private val keyguardStateController: KeyguardStateController,
107     private val bypassController: KeyguardBypassController,
108     private val mediaCarouselController: MediaCarouselController,
109     private val mediaManager: MediaDataManager,
110     private val keyguardViewController: KeyguardViewController,
111     private val dreamOverlayStateController: DreamOverlayStateController,
112     private val keyguardInteractor: KeyguardInteractor,
113     communalTransitionViewModel: CommunalTransitionViewModel,
114     configurationController: ConfigurationController,
115     wakefulnessLifecycle: WakefulnessLifecycle,
116     shadeInteractor: ShadeInteractor,
117     private val secureSettings: SecureSettings,
118     @Main private val handler: Handler,
119     @Application private val coroutineScope: CoroutineScope,
120     private val splitShadeStateController: SplitShadeStateController,
121     private val logger: MediaViewLogger,
122 ) {
123 
124     /** Track the media player setting status on lock screen. */
125     private var allowMediaPlayerOnLockScreen: Boolean = true
126     private val lockScreenMediaPlayerUri =
127         secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
128 
129     /**
130      * Whether we "skip" QQS during panel expansion.
131      *
132      * This means that when expanding the panel we go directly to QS. Also when we are on QS and
133      * start closing the panel, it fully collapses instead of going to QQS.
134      */
135     private var skipQqsOnExpansion: Boolean = false
136 
137     /**
138      * The root overlay of the hierarchy. This is where the media notification is attached to
139      * whenever the view is transitioning from one host to another. It also make sure that the view
140      * is always in its final state when it is attached to a view host.
141      */
142     private var rootOverlay: ViewGroupOverlay? = null
143 
144     private var rootView: View? = null
145     private var currentBounds = Rect()
146     private var animationStartBounds: Rect = Rect()
147 
148     private var animationStartClipping = Rect()
149     private var currentClipping = Rect()
150     private var targetClipping = Rect()
151 
152     /**
153      * The cross fade progress at the start of the animation. 0.5f means it's just switching between
154      * the start and the end location and the content is fully faded, while 0.75f means that we're
155      * halfway faded in again in the target state.
156      */
157     private var animationStartCrossFadeProgress = 0.0f
158 
159     /** The starting alpha of the animation */
160     private var animationStartAlpha = 0.0f
161 
162     /** The starting location of the cross fade if an animation is running right now. */
163     @MediaLocation private var crossFadeAnimationStartLocation = LOCATION_UNKNOWN
164 
165     /** The end location of the cross fade if an animation is running right now. */
166     @MediaLocation private var crossFadeAnimationEndLocation = LOCATION_UNKNOWN
167     private var targetBounds: Rect = Rect()
168     private val mediaFrame
169         get() = mediaCarouselController.mediaFrame
170 
171     private var statusbarState: Int = statusBarStateController.state
172     private var animator =
<lambda>null173         ValueAnimator.ofFloat(0.0f, 1.0f).apply {
174             interpolator = Interpolators.FAST_OUT_SLOW_IN
175             addUpdateListener {
176                 updateTargetState()
177                 val currentAlpha: Float
178                 var boundsProgress = animatedFraction
179                 if (isCrossFadeAnimatorRunning) {
180                     animationCrossFadeProgress =
181                         MathUtils.lerp(animationStartCrossFadeProgress, 1.0f, animatedFraction)
182                     // When crossfading, let's keep the bounds at the right location during fading
183                     boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f
184                     currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress)
185                 } else {
186                     // If we're not crossfading, let's interpolate from the start alpha to 1.0f
187                     currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction)
188                 }
189                 interpolateBounds(
190                     animationStartBounds,
191                     targetBounds,
192                     boundsProgress,
193                     result = currentBounds,
194                 )
195                 resolveClipping(currentClipping)
196                 applyState(currentBounds, currentAlpha, clipBounds = currentClipping)
197             }
198             addListener(
199                 object : AnimatorListenerAdapter() {
200                     private var cancelled: Boolean = false
201 
202                     override fun onAnimationCancel(animation: Animator) {
203                         cancelled = true
204                         animationPending = false
205                         rootView?.removeCallbacks(startAnimation)
206                     }
207 
208                     override fun onAnimationEnd(animation: Animator) {
209                         isCrossFadeAnimatorRunning = false
210                         if (!cancelled) {
211                             applyTargetStateIfNotAnimating()
212                         }
213                     }
214 
215                     override fun onAnimationStart(animation: Animator) {
216                         cancelled = false
217                         animationPending = false
218                     }
219                 }
220             )
221         }
222 
resolveClippingnull223     private fun resolveClipping(result: Rect) {
224         if (animationStartClipping.isEmpty) result.set(targetClipping)
225         else if (targetClipping.isEmpty) result.set(animationStartClipping)
226         else result.setIntersect(animationStartClipping, targetClipping)
227     }
228 
229     private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_COMMUNAL_HUB + 1)
230 
231     /**
232      * The last location where this view was at before going to the desired location. This is useful
233      * for guided transitions.
234      */
235     @MediaLocation private var previousLocation = LOCATION_UNKNOWN
236     /** The desired location where the view will be at the end of the transition. */
237     @MediaLocation private var desiredLocation = LOCATION_UNKNOWN
238 
239     /**
240      * The current attachment location where the view is currently attached. Usually this matches
241      * the desired location except for animations whenever a view moves to the new desired location,
242      * during which it is in [IN_OVERLAY].
243      */
244     @MediaLocation private var currentAttachmentLocation = LOCATION_UNKNOWN
245 
246     private var inSplitShade = false
247 
248     /**
249      * Whether we are transitioning to the hub or from the hub to the shade. If so, use fade as the
250      * transformation type and skip calculating state with the bounds and the transition progress.
251      */
252     private val isHubTransition
253         get() =
254             desiredLocation == LOCATION_COMMUNAL_HUB ||
255                 (previousLocation == LOCATION_COMMUNAL_HUB && desiredLocation == LOCATION_QS)
256 
257     /** Is there any active media or recommendation in the carousel? */
258     private var hasActiveMediaOrRecommendation: Boolean = false
259         get() = mediaManager.hasActiveMediaOrRecommendation()
260 
261     /** Are we currently waiting on an animation to start? */
262     private var animationPending: Boolean = false
<lambda>null263     private val startAnimation: Runnable = Runnable { animator.start() }
264 
265     /** The expansion of quick settings */
266     var qsExpansion: Float = 0.0f
267         set(value) {
268             if (field != value) {
269                 field = value
270                 updateDesiredLocation()
271                 if (getQSTransformationProgress() >= 0) {
272                     updateTargetState()
273                     applyTargetStateIfNotAnimating()
274                 }
275             }
276         }
277 
278     /** Is quick setting expanded? */
279     var qsExpanded: Boolean = false
280         set(value) {
281             if (field != value) {
282                 field = value
283                 mediaCarouselController.mediaCarouselScrollHandler.qsExpanded = value
284             }
285             // qs is expanded on LS shade and HS shade
286             if (value && (isLockScreenShadeVisibleToUser() || isHomeScreenShadeVisibleToUser())) {
287                 mediaCarouselController.logSmartspaceImpression(value)
288             }
289             updateUserVisibility()
290         }
291 
292     /**
293      * distance that the full shade transition takes in order for media to fully transition to the
294      * shade
295      */
296     private var distanceForFullShadeTransition = 0
297 
298     /**
299      * The amount of progress we are currently in if we're transitioning to the full shade. 0.0f
300      * means we're not transitioning yet, while 1 means we're all the way in the full shade.
301      */
302     private var fullShadeTransitionProgress = 0f
303         set(value) {
304             if (field == value) {
305                 return
306             }
307             field = value
308             if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) {
309                 // No need to do all the calculations / updates below if we're not on the lockscreen
310                 // or if we're bypassing.
311                 return
312             }
313             updateDesiredLocation(forceNoAnimation = isCurrentlyFading())
314             if (value >= 0) {
315                 updateTargetState()
316                 // Setting the alpha directly, as the below call will use it to update the alpha
317                 carouselAlpha = calculateAlphaFromCrossFade(field)
318                 applyTargetStateIfNotAnimating()
319             }
320         }
321 
322     /** Is there currently a cross-fade animation running driven by an animator? */
323     private var isCrossFadeAnimatorRunning = false
324 
325     /**
326      * Are we currently transitionioning from the lockscreen to the full shade
327      * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and
328      * the transition starts, this will no longer return true.
329      */
330     private val isTransitioningToFullShade: Boolean
331         get() =
332             fullShadeTransitionProgress != 0f &&
333                 !bypassController.bypassEnabled &&
334                 statusbarState == StatusBarState.KEYGUARD
335 
336     /**
337      * Set the amount of pixels we have currently dragged down if we're transitioning to the full
338      * shade. 0.0f means we're not transitioning yet.
339      */
setTransitionToFullShadeAmountnull340     fun setTransitionToFullShadeAmount(value: Float) {
341         // If we're transitioning starting on the shade_locked, we don't want any delay and rather
342         // have it aligned with the rest of the animation
343         val progress = MathUtils.saturate(value / distanceForFullShadeTransition)
344         fullShadeTransitionProgress = progress
345     }
346 
347     /**
348      * Returns the amount of translationY of the media container, during the current guided
349      * transformation, if running. If there is no guided transformation running, it will return -1.
350      */
getGuidedTransformationTranslationYnull351     fun getGuidedTransformationTranslationY(): Int {
352         if (!isCurrentlyInGuidedTransformation()) {
353             return -1
354         }
355         val startHost = getHost(previousLocation)
356         if (startHost == null || !startHost.visible) {
357             return 0
358         }
359         return targetBounds.top - startHost.currentBounds.top
360     }
361 
362     /**
363      * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
364      * we wouldn't want to transition in that case.
365      */
366     var collapsingShadeFromQS: Boolean = false
367         set(value) {
368             if (field != value) {
369                 field = value
370                 updateDesiredLocation(forceNoAnimation = true)
371             }
372         }
373 
374     /** Are location changes currently blocked? */
375     private val blockLocationChanges: Boolean
376         get() {
377             return goingToSleep || dozeAnimationRunning
378         }
379 
380     /** Are we currently going to sleep */
381     private var goingToSleep: Boolean = false
382         set(value) {
383             if (field != value) {
384                 field = value
385                 if (!value) {
386                     updateDesiredLocation()
387                 }
388             }
389         }
390 
391     /** Are we currently fullyAwake */
392     private var fullyAwake: Boolean = false
393         set(value) {
394             if (field != value) {
395                 field = value
396                 if (value) {
397                     updateDesiredLocation(forceNoAnimation = true)
398                 }
399             }
400         }
401 
402     /** Is the doze animation currently Running */
403     private var dozeAnimationRunning: Boolean = false
404         private set(value) {
405             if (field != value) {
406                 field = value
407                 if (!value) {
408                     updateDesiredLocation()
409                 }
410             }
411         }
412 
413     /** Is the dream overlay currently active */
414     private var dreamOverlayActive: Boolean = false
415         private set(value) {
416             if (field != value) {
417                 field = value
418                 updateDesiredLocation(forceNoAnimation = true)
419             }
420         }
421 
422     /** Is the dream media complication currently active */
423     private var dreamMediaComplicationActive: Boolean = false
424         private set(value) {
425             if (field != value) {
426                 field = value
427                 updateDesiredLocation(forceNoAnimation = true)
428             }
429         }
430 
431     /** Is the communal UI showing */
432     private var isCommunalShowing: Boolean = false
433 
434     /** Is the primary bouncer showing */
435     private var isPrimaryBouncerShowing: Boolean = false
436 
437     /** Is either shade or QS fully expanded */
438     private var isAnyShadeFullyExpanded: Boolean = false
439 
440     /** Is the communal UI showing and not dreaming */
441     private var onCommunalNotDreaming: Boolean = false
442 
443     /** Is the communal UI showing, dreaming and shade expanding */
444     private var onCommunalDreamingAndShadeExpanding: Boolean = false
445 
446     /**
447      * The current cross fade progress. 0.5f means it's just switching between the start and the end
448      * location and the content is fully faded, while 0.75f means that we're halfway faded in again
449      * in the target state. This is only valid while [isCrossFadeAnimatorRunning] is true.
450      */
451     private var animationCrossFadeProgress = 1.0f
452 
453     /** The current carousel Alpha. */
454     private var carouselAlpha: Float = 1.0f
455         set(value) {
456             if (field == value) {
457                 return
458             }
459             field = value
460             CrossFadeHelper.fadeIn(mediaFrame, value)
461         }
462 
463     /**
464      * Calculate the alpha of the view when given a cross-fade progress.
465      *
466      * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching
467      *   between the start and the end location and the content is fully faded, while 0.75f means
468      *   that we're halfway faded in again in the target state.
469      */
calculateAlphaFromCrossFadenull470     private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float {
471         if (crossFadeProgress <= 0.5f) {
472             return 1.0f - crossFadeProgress / 0.5f
473         } else {
474             return (crossFadeProgress - 0.5f) / 0.5f
475         }
476     }
477 
478     init {
479         updateConfiguration()
480         configurationController.addCallback(
481             object : ConfigurationController.ConfigurationListener {
onConfigChangednull482                 override fun onConfigChanged(newConfig: Configuration?) {
483                     updateConfiguration()
484                     updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true)
485                 }
486             }
487         )
488         statusBarStateController.addCallback(
489             object : StatusBarStateController.StateListener {
onStatePreChangenull490                 override fun onStatePreChange(oldState: Int, newState: Int) {
491                     // We're updating the location before the state change happens, since we want
492                     // the location of the previous state to still be up to date when the animation
493                     // starts
494                     if (
495                         newState == StatusBarState.SHADE_LOCKED &&
496                             oldState == StatusBarState.KEYGUARD &&
497                             fullShadeTransitionProgress < 1.0f
498                     ) {
499                         // Since the new state is SHADE_LOCKED, we need to set the transition amount
500                         // to maximum if the progress is not 1f.
501                         setTransitionToFullShadeAmount(distanceForFullShadeTransition.toFloat())
502                     }
503                     statusbarState = newState
504                     updateDesiredLocation()
505                 }
506 
onStateChangednull507                 override fun onStateChanged(newState: Int) {
508                     updateTargetState()
509                     // Enters shade from lock screen
510                     if (
511                         newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser()
512                     ) {
513                         mediaCarouselController.logSmartspaceImpression(qsExpanded)
514                     }
515                     updateUserVisibility()
516                 }
517 
onDozeAmountChangednull518                 override fun onDozeAmountChanged(linear: Float, eased: Float) {
519                     dozeAnimationRunning = linear != 0.0f && linear != 1.0f
520                 }
521 
onDozingChangednull522                 override fun onDozingChanged(isDozing: Boolean) {
523                     if (!isDozing) {
524                         dozeAnimationRunning = false
525                         // Enters lock screen from screen off
526                         if (isLockScreenVisibleToUser()) {
527                             mediaCarouselController.logSmartspaceImpression(qsExpanded)
528                         }
529                     } else {
530                         updateDesiredLocation()
531                         qsExpanded = false
532                         closeGuts()
533                     }
534                     updateUserVisibility()
535                 }
536 
onExpandedChangednull537                 override fun onExpandedChanged(isExpanded: Boolean) {
538                     // Enters shade from home screen
539                     if (isHomeScreenShadeVisibleToUser()) {
540                         mediaCarouselController.logSmartspaceImpression(qsExpanded)
541                     }
542                     updateUserVisibility()
543                 }
544             }
545         )
546 
547         dreamOverlayStateController.addCallback(
548             object : DreamOverlayStateController.Callback {
onComplicationsChangednull549                 override fun onComplicationsChanged() {
550                     dreamMediaComplicationActive =
551                         dreamOverlayStateController.complications.any {
552                             it is MediaDreamComplication
553                         }
554                 }
555 
onStateChangednull556                 override fun onStateChanged() {
557                     dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it }
558                 }
559             }
560         )
561 
562         wakefulnessLifecycle.addObserver(
563             object : WakefulnessLifecycle.Observer {
onFinishedGoingToSleepnull564                 override fun onFinishedGoingToSleep() {
565                     goingToSleep = false
566                 }
567 
onStartedGoingToSleepnull568                 override fun onStartedGoingToSleep() {
569                     goingToSleep = true
570                     fullyAwake = false
571                 }
572 
onFinishedWakingUpnull573                 override fun onFinishedWakingUp() {
574                     goingToSleep = false
575                     fullyAwake = true
576                 }
577 
onStartedWakingUpnull578                 override fun onStartedWakingUp() {
579                     goingToSleep = false
580                 }
581             }
582         )
583 
584         mediaCarouselController.updateUserVisibility = this::updateUserVisibility
<lambda>null585         mediaCarouselController.updateHostVisibility = {
586             mediaHosts.forEach { it?.updateViewVisibility() }
587         }
588 
<lambda>null589         coroutineScope.launch {
590             shadeInteractor.isQsBypassingShade.collect { isExpandImmediateEnabled ->
591                 skipQqsOnExpansion = isExpandImmediateEnabled
592                 updateDesiredLocation()
593             }
594         }
595 
<lambda>null596         coroutineScope.launch {
597             shadeInteractor.isAnyFullyExpanded.collect {
598                 isAnyShadeFullyExpanded = it
599                 updateUserVisibility()
600             }
601         }
602 
<lambda>null603         coroutineScope.launch {
604             keyguardInteractor.primaryBouncerShowing.collect {
605                 isPrimaryBouncerShowing = it
606                 updateUserVisibility()
607             }
608         }
609 
610         if (mediaControlsLockscreenShadeBugFix()) {
<lambda>null611             coroutineScope.launch {
612                 shadeInteractor.shadeExpansion.collect { expansion ->
613                     if (expansion >= 1f || expansion <= 0f) {
614                         // Shade has fully expanded or collapsed: force transition amount update
615                         setTransitionToFullShadeAmount(expansion)
616                     }
617                 }
618             }
619         }
620 
621         val settingsObserver: ContentObserver =
622             object : ContentObserver(handler) {
onChangenull623                 override fun onChange(selfChange: Boolean, uri: Uri?) {
624                     if (uri == lockScreenMediaPlayerUri) {
625                         allowMediaPlayerOnLockScreen =
626                             secureSettings.getBoolForUser(
627                                 Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
628                                 true,
629                                 UserHandle.USER_CURRENT,
630                             )
631                     }
632                 }
633             }
634         secureSettings.registerContentObserverForUserSync(
635             Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
636             settingsObserver,
637             UserHandle.USER_ALL,
638         )
639 
640         // Listen to the communal UI state. Make sure that communal UI is showing and hub itself is
641         // available, ie. not disabled and able to be shown.
642         // When dreaming, qs expansion is immediately set to 1f, so we listen to shade expansion to
643         // calculate the new location.
<lambda>null644         coroutineScope.launch {
645             combine(
646                     communalTransitionViewModel.isUmoOnCommunal,
647                     keyguardInteractor.isDreaming,
648                     // keep on communal before the shade is expanded enough to show the elements in
649                     // QS
650                     shadeInteractor.shadeExpansion
651                         .mapLatest { it < EXPANSION_THRESHOLD }
652                         .distinctUntilChanged(),
653                     ::Triple,
654                 )
655                 .collectLatest { (communalShowing, isDreaming, isShadeExpanding) ->
656                     isCommunalShowing = communalShowing
657                     onCommunalDreamingAndShadeExpanding =
658                         communalShowing && isDreaming && isShadeExpanding
659                     onCommunalNotDreaming = communalShowing && !isDreaming
660                     updateDesiredLocation(forceNoAnimation = true)
661                     updateUserVisibility()
662                 }
663         }
664     }
665 
updateConfigurationnull666     private fun updateConfiguration() {
667         distanceForFullShadeTransition =
668             context.resources.getDimensionPixelSize(
669                 R.dimen.lockscreen_shade_media_transition_distance
670             )
671         inSplitShade = splitShadeStateController.shouldUseSplitNotificationShade(context.resources)
672     }
673 
674     /**
675      * Register a media host and create a view can be attached to a view hierarchy and where the
676      * players will be placed in when the host is the currently desired state.
677      *
678      * @return the hostView associated with this location
679      */
registernull680     fun register(mediaObject: MediaHost): UniqueObjectHostView {
681         val viewHost = createUniqueObjectHost()
682         mediaObject.hostView = viewHost
683         mediaObject.addVisibilityChangeListener {
684             // Never animate because of a visibility change, only state changes should do that
685             updateDesiredLocation(forceNoAnimation = true)
686         }
687         mediaHosts[mediaObject.location] = mediaObject
688         if (mediaObject.location == desiredLocation) {
689             // In case we are overriding a view that is already visible, make sure we attach it
690             // to this new host view in the below call
691             desiredLocation = LOCATION_UNKNOWN
692         }
693         if (mediaObject.location == currentAttachmentLocation) {
694             currentAttachmentLocation = LOCATION_UNKNOWN
695         }
696         updateDesiredLocation()
697         return viewHost
698     }
699 
700     /** Close the guts in all players in [MediaCarouselController]. */
closeGutsnull701     fun closeGuts() {
702         mediaCarouselController.closeGuts()
703     }
704 
createUniqueObjectHostnull705     private fun createUniqueObjectHost(): UniqueObjectHostView {
706         val viewHost = UniqueObjectHostView(context)
707         viewHost.addOnAttachStateChangeListener(
708             object : View.OnAttachStateChangeListener {
709                 override fun onViewAttachedToWindow(p0: View) {
710                     if (rootOverlay == null) {
711                         rootView = viewHost.viewRootImpl.view
712                         rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
713                     }
714                     viewHost.removeOnAttachStateChangeListener(this)
715                 }
716 
717                 override fun onViewDetachedFromWindow(p0: View) {}
718             }
719         )
720         return viewHost
721     }
722 
723     /**
724      * Updates the location that the view should be in. If it changes, an animation may be triggered
725      * going from the old desired location to the new one.
726      *
727      * @param forceNoAnimation optional parameter telling the system not to animate
728      * @param forceStateUpdate optional parameter telling the system to update transition state
729      *
730      * ```
731      *                         even if location did not change
732      * ```
733      */
updateDesiredLocationnull734     private fun updateDesiredLocation(
735         forceNoAnimation: Boolean = false,
736         forceStateUpdate: Boolean = false,
737     ) =
738         traceSection("MediaHierarchyManager#updateDesiredLocation") {
739             val desiredLocation = calculateLocation()
740             if (
741                 desiredLocation != this.desiredLocation || forceStateUpdate && !blockLocationChanges
742             ) {
743                 if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) {
744                     // Only update previous location when it actually changes
745                     previousLocation = this.desiredLocation
746                 } else if (forceStateUpdate) {
747                     val onLockscreen =
748                         (!bypassController.bypassEnabled &&
749                             (statusbarState == StatusBarState.KEYGUARD))
750                     if (
751                         desiredLocation == LOCATION_QS &&
752                             previousLocation == LOCATION_LOCKSCREEN &&
753                             !onLockscreen
754                     ) {
755                         // If media active state changed and the device is now unlocked, update the
756                         // previous location so we animate between the correct hosts
757                         previousLocation = LOCATION_QQS
758                     }
759                 }
760                 val isNewView = this.desiredLocation == LOCATION_UNKNOWN
761                 this.desiredLocation = desiredLocation
762                 // Let's perform a transition
763                 val animate =
764                     !forceNoAnimation && shouldAnimateTransition(desiredLocation, previousLocation)
765                 val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
766                 val host = getHost(desiredLocation)
767                 val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE
768                 if (!willFade || isCurrentlyInGuidedTransformation() || !animate) {
769                     // if we're fading, we want the desired location / measurement only to change
770                     // once fully faded. This is happening in the host attachment
771                     mediaCarouselController.onDesiredLocationChanged(
772                         desiredLocation,
773                         host,
774                         animate,
775                         animDuration,
776                         delay,
777                     )
778                 }
779                 performTransitionToNewLocation(isNewView, animate)
780             }
781         }
782 
performTransitionToNewLocationnull783     private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) =
784         traceSection("MediaHierarchyManager#performTransitionToNewLocation") {
785             if (previousLocation < 0 || isNewView) {
786                 cancelAnimationAndApplyDesiredState()
787                 return
788             }
789             val currentHost = getHost(desiredLocation)
790             val previousHost = getHost(previousLocation)
791             if (currentHost == null || previousHost == null) {
792                 cancelAnimationAndApplyDesiredState()
793                 return
794             }
795             updateTargetState()
796             if (isCurrentlyInGuidedTransformation()) {
797                 applyTargetStateIfNotAnimating()
798             } else if (animate) {
799                 val wasCrossFading = isCrossFadeAnimatorRunning
800                 val previewsCrossFadeProgress = animationCrossFadeProgress
801                 animator.cancel()
802                 if (
803                     currentAttachmentLocation != previousLocation ||
804                         !previousHost.hostView.isAttachedToWindow
805                 ) {
806                     // Let's animate to the new position, starting from the current position
807                     // We also go in here in case the view was detached, since the bounds wouldn't
808                     // be correct anymore
809                     animationStartBounds.set(currentBounds)
810                     animationStartClipping.set(currentClipping)
811                 } else {
812                     // otherwise, let's take the freshest state, since the current one could
813                     // be outdated
814                     animationStartBounds.set(previousHost.currentBounds)
815                     animationStartClipping.set(previousHost.currentClipping)
816                 }
817                 val transformationType = calculateTransformationType()
818                 var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE
819                 var crossFadeStartProgress = 0.0f
820                 // The alpha is only relevant when not cross fading
821                 var newCrossFadeStartLocation = previousLocation
822                 if (wasCrossFading) {
823                     if (currentAttachmentLocation == crossFadeAnimationEndLocation) {
824                         if (needsCrossFade) {
825                             // We were previously crossFading and we've already reached
826                             // the end view, Let's start crossfading from the same position there
827                             crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
828                         }
829                         // Otherwise let's fade in from the current alpha, but not cross fade
830                     } else {
831                         // We haven't reached the previous location yet, let's still cross fade from
832                         // where we were.
833                         newCrossFadeStartLocation = crossFadeAnimationStartLocation
834                         if (newCrossFadeStartLocation == desiredLocation) {
835                             // we're crossFading back to where we were, let's start at the end
836                             // position
837                             crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
838                         } else {
839                             // Let's start from where we are right now
840                             crossFadeStartProgress = previewsCrossFadeProgress
841                             // We need to force cross fading as we haven't reached the end location
842                             // yet
843                             needsCrossFade = true
844                         }
845                     }
846                 } else if (needsCrossFade) {
847                     // let's not flicker and start with the same alpha
848                     crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f
849                 }
850                 isCrossFadeAnimatorRunning = needsCrossFade
851                 crossFadeAnimationStartLocation = newCrossFadeStartLocation
852                 crossFadeAnimationEndLocation = desiredLocation
853                 animationStartAlpha = carouselAlpha
854                 animationStartCrossFadeProgress = crossFadeStartProgress
855                 adjustAnimatorForTransition(desiredLocation, previousLocation)
856                 if (!animationPending) {
857                     rootView?.let {
858                         // Let's delay the animation start until we finished laying out
859                         animationPending = true
860                         it.postOnAnimation(startAnimation)
861                     }
862                 }
863             } else {
864                 cancelAnimationAndApplyDesiredState()
865             }
866         }
867 
shouldAnimateTransitionnull868     private fun shouldAnimateTransition(
869         @MediaLocation currentLocation: Int,
870         @MediaLocation previousLocation: Int,
871     ): Boolean {
872         if (isCurrentlyInGuidedTransformation()) {
873             return false
874         }
875         if (skipQqsOnExpansion) {
876             return false
877         }
878         if (isHubTransition) {
879             return false
880         }
881         // This is an invalid transition, and can happen when using the camera gesture from the
882         // lock screen. Disallow.
883         if (
884             previousLocation == LOCATION_LOCKSCREEN &&
885                 desiredLocation == LOCATION_QQS &&
886                 statusbarState == StatusBarState.SHADE
887         ) {
888             return false
889         }
890 
891         if (
892             currentLocation == LOCATION_QQS &&
893                 previousLocation == LOCATION_LOCKSCREEN &&
894                 (statusBarStateController.leaveOpenOnKeyguardHide() ||
895                     statusbarState == StatusBarState.SHADE_LOCKED)
896         ) {
897             // Usually listening to the isShown is enough to determine this, but there is some
898             // non-trivial reattaching logic happening that will make the view not-shown earlier
899             return true
900         }
901 
902         if (
903             desiredLocation == LOCATION_QS &&
904                 previousLocation == LOCATION_LOCKSCREEN &&
905                 statusbarState == StatusBarState.SHADE
906         ) {
907             // This is an invalid transition, can happen when tapping on home control and the UMO
908             // while being on landscape orientation in tablet.
909             return false
910         }
911 
912         if (
913             statusbarState == StatusBarState.KEYGUARD &&
914                 (currentLocation == LOCATION_LOCKSCREEN || previousLocation == LOCATION_LOCKSCREEN)
915         ) {
916             // We're always fading from lockscreen to keyguard in situations where the player
917             // is already fully hidden
918             return false
919         }
920         return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
921     }
922 
adjustAnimatorForTransitionnull923     private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
924         val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
925         animator.apply {
926             duration = animDuration
927             startDelay = delay
928         }
929     }
930 
getAnimationParamsnull931     private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
932         var animDuration = 200L
933         var delay = 0L
934         if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
935             // Going to the full shade, let's adjust the animation duration
936             if (
937                 statusbarState == StatusBarState.SHADE &&
938                     keyguardStateController.isKeyguardFadingAway
939             ) {
940                 delay = keyguardStateController.keyguardFadingAwayDelay
941             }
942             animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong()
943         } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
944             animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
945         }
946         return animDuration to delay
947     }
948 
applyTargetStateIfNotAnimatingnull949     private fun applyTargetStateIfNotAnimating() {
950         if (!animator.isRunning) {
951             // Let's immediately apply the target state (which is interpolated) if there is
952             // no animation running. Otherwise the animation update will already update
953             // the location
954             applyState(targetBounds, carouselAlpha, clipBounds = targetClipping)
955         }
956     }
957 
958     /** Updates the bounds that the view wants to be in at the end of the animation. */
updateTargetStatenull959     private fun updateTargetState() {
960         var starthost = getHost(previousLocation)
961         var endHost = getHost(desiredLocation)
962         if (
963             isCurrentlyInGuidedTransformation() &&
964                 !isCurrentlyFading() &&
965                 starthost != null &&
966                 endHost != null
967         ) {
968             val progress = getTransformationProgress()
969             // If either of the hosts are invisible, let's keep them at the other host location to
970             // have a nicer disappear animation. Otherwise the currentBounds of the state might
971             // be undefined
972             if (!endHost.visible) {
973                 endHost = starthost
974             } else if (!starthost.visible) {
975                 starthost = endHost
976             }
977             val newBounds = endHost.currentBounds
978             val previousBounds = starthost.currentBounds
979             targetBounds = interpolateBounds(previousBounds, newBounds, progress)
980             targetClipping = endHost.currentClipping
981         } else if (endHost != null) {
982             val bounds = endHost.currentBounds
983             targetBounds.set(bounds)
984             targetClipping = endHost.currentClipping
985         }
986     }
987 
interpolateBoundsnull988     private fun interpolateBounds(
989         startBounds: Rect,
990         endBounds: Rect,
991         progress: Float,
992         result: Rect? = null,
993     ): Rect {
994         val left =
995             MathUtils.lerp(startBounds.left.toFloat(), endBounds.left.toFloat(), progress).toInt()
996         val top =
997             MathUtils.lerp(startBounds.top.toFloat(), endBounds.top.toFloat(), progress).toInt()
998         val right =
999             MathUtils.lerp(startBounds.right.toFloat(), endBounds.right.toFloat(), progress).toInt()
1000         val bottom =
1001             MathUtils.lerp(startBounds.bottom.toFloat(), endBounds.bottom.toFloat(), progress)
1002                 .toInt()
1003         val resultBounds = result ?: Rect()
1004         resultBounds.set(left, top, right, bottom)
1005         return resultBounds
1006     }
1007 
1008     /** @return true if this transformation is guided by an external progress like a finger */
isCurrentlyInGuidedTransformationnull1009     fun isCurrentlyInGuidedTransformation(): Boolean {
1010         return hasValidStartAndEndLocations() &&
1011             getTransformationProgress() >= 0 &&
1012             (areGuidedTransitionHostsVisible() || !hasActiveMediaOrRecommendation)
1013     }
1014 
hasValidStartAndEndLocationsnull1015     private fun hasValidStartAndEndLocations(): Boolean {
1016         return previousLocation != LOCATION_UNKNOWN && desiredLocation != LOCATION_UNKNOWN
1017     }
1018 
1019     /** Calculate the transformation type for the current animation */
1020     @VisibleForTesting
1021     @TransformationType
calculateTransformationTypenull1022     fun calculateTransformationType(): Int {
1023         if (isHubTransition) {
1024             return TRANSFORMATION_TYPE_FADE
1025         }
1026         if (isTransitioningToFullShade) {
1027             if (inSplitShade && areGuidedTransitionHostsVisible()) {
1028                 return TRANSFORMATION_TYPE_TRANSITION
1029             }
1030             return TRANSFORMATION_TYPE_FADE
1031         }
1032         if (
1033             previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS ||
1034                 previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN
1035         ) {
1036             // animating between ls and qs should fade, as QS is clipped.
1037             return TRANSFORMATION_TYPE_FADE
1038         }
1039         if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
1040             // animating between ls and qqs should fade when dragging down via e.g. expand button
1041             return TRANSFORMATION_TYPE_FADE
1042         }
1043         return TRANSFORMATION_TYPE_TRANSITION
1044     }
1045 
areGuidedTransitionHostsVisiblenull1046     private fun areGuidedTransitionHostsVisible(): Boolean {
1047         return getHost(previousLocation)?.visible == true &&
1048             getHost(desiredLocation)?.visible == true
1049     }
1050 
1051     /**
1052      * @return the current transformation progress if we're in a guided transformation and -1
1053      *   otherwise
1054      */
getTransformationProgressnull1055     private fun getTransformationProgress(): Float {
1056         if (skipQqsOnExpansion || isHubTransition) {
1057             return -1.0f
1058         }
1059         val progress = getQSTransformationProgress()
1060         if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
1061             return progress
1062         }
1063         if (isTransitioningToFullShade) {
1064             return fullShadeTransitionProgress
1065         }
1066         return -1.0f
1067     }
1068 
getQSTransformationProgressnull1069     private fun getQSTransformationProgress(): Float {
1070         val currentHost = getHost(desiredLocation)
1071         val previousHost = getHost(previousLocation)
1072         if (currentHost?.location == LOCATION_QS && !inSplitShade) {
1073             if (previousHost?.location == LOCATION_QQS) {
1074                 if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
1075                     return qsExpansion
1076                 }
1077             }
1078         }
1079         return -1.0f
1080     }
1081 
getHostnull1082     private fun getHost(@MediaLocation location: Int): MediaHost? {
1083         if (location < 0) {
1084             return null
1085         }
1086         return mediaHosts[location]
1087     }
1088 
cancelAnimationAndApplyDesiredStatenull1089     private fun cancelAnimationAndApplyDesiredState() {
1090         animator.cancel()
1091         getHost(desiredLocation)?.let {
1092             applyState(it.currentBounds, alpha = 1.0f, immediately = true)
1093         }
1094     }
1095 
1096     /** Apply the current state to the view, updating it's bounds and desired state */
applyStatenull1097     private fun applyState(
1098         bounds: Rect,
1099         alpha: Float,
1100         immediately: Boolean = false,
1101         clipBounds: Rect = EMPTY_RECT,
1102     ) =
1103         traceSection("MediaHierarchyManager#applyState") {
1104             currentBounds.set(bounds)
1105             currentClipping = clipBounds
1106             carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f
1107             val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading()
1108             val startLocation = if (onlyUseEndState) LOCATION_UNKNOWN else previousLocation
1109             val progress = if (onlyUseEndState) 1.0f else getTransformationProgress()
1110             val endLocation = resolveLocationForFading()
1111             mediaCarouselController.setCurrentState(
1112                 startLocation,
1113                 endLocation,
1114                 progress,
1115                 immediately,
1116             )
1117             updateHostAttachment()
1118             if (currentAttachmentLocation == IN_OVERLAY) {
1119                 // Setting the clipping on the hierarchy of `mediaFrame` does not work
1120                 if (!currentClipping.isEmpty) {
1121                     currentBounds.intersect(currentClipping)
1122                 }
1123                 mediaFrame.setLeftTopRightBottom(
1124                     currentBounds.left,
1125                     currentBounds.top,
1126                     currentBounds.right,
1127                     currentBounds.bottom,
1128                 )
1129             }
1130         }
1131 
updateHostAttachmentnull1132     private fun updateHostAttachment() =
1133         traceSection("MediaHierarchyManager#updateHostAttachment") {
1134             if (SceneContainerFlag.isEnabled) {
1135                 // No need to manage transition states - just update the desired location directly
1136                 logger.logMediaHostAttachment(desiredLocation)
1137                 mediaCarouselController.onDesiredLocationChanged(
1138                     desiredLocation = desiredLocation,
1139                     desiredHostState = getHost(desiredLocation),
1140                     animate = false,
1141                 )
1142                 return
1143             }
1144 
1145             var newLocation = resolveLocationForFading()
1146             // Don't use the overlay when fading or when we don't have active media
1147             var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation
1148             if (isCrossFadeAnimatorRunning) {
1149                 if (
1150                     getHost(newLocation)?.visible == true &&
1151                         getHost(newLocation)?.hostView?.isShown == false &&
1152                         newLocation != desiredLocation
1153                 ) {
1154                     // We're crossfading but the view is already hidden. Let's move to the overlay
1155                     // instead. This happens when animating to the full shade using a button click.
1156                     canUseOverlay = true
1157                 }
1158             }
1159             val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay
1160             newLocation = if (inOverlay) IN_OVERLAY else newLocation
1161             if (currentAttachmentLocation != newLocation) {
1162                 currentAttachmentLocation = newLocation
1163 
1164                 // Remove the carousel from the old host
1165                 (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
1166 
1167                 // Add it to the new one
1168                 if (inOverlay) {
1169                     rootOverlay!!.add(mediaFrame)
1170                 } else {
1171                     val targetHost = getHost(newLocation)!!.hostView
1172                     // This will either do a full layout pass and remeasure, or it will bypass
1173                     // that and directly set the mediaFrame's bounds within the premeasured host.
1174                     targetHost.addView(mediaFrame)
1175                 }
1176                 logger.logMediaHostAttachment(currentAttachmentLocation)
1177                 if (isCrossFadeAnimatorRunning) {
1178                     // When cross-fading with an animation, we only notify the media carousel of the
1179                     // location change, once the view is reattached to the new place and not
1180                     // immediately
1181                     // when the desired location changes. This callback will update the measurement
1182                     // of the carousel, only once we've faded out at the old location and then
1183                     // reattach
1184                     // to fade it in at the new location.
1185                     mediaCarouselController.onDesiredLocationChanged(
1186                         newLocation,
1187                         getHost(newLocation),
1188                         animate = false,
1189                     )
1190                 }
1191             }
1192         }
1193 
1194     /**
1195      * Calculate the location when cross fading between locations. While fading out, the content
1196      * should remain in the previous location, while after the switch it should be at the desired
1197      * location.
1198      */
resolveLocationForFadingnull1199     private fun resolveLocationForFading(): Int {
1200         if (isCrossFadeAnimatorRunning) {
1201             // When animating between two hosts with a fade, let's keep ourselves in the old
1202             // location for the first half, and then switch over to the end location
1203             if (animationCrossFadeProgress > 0.5 || previousLocation == LOCATION_UNKNOWN) {
1204                 return crossFadeAnimationEndLocation
1205             } else {
1206                 return crossFadeAnimationStartLocation
1207             }
1208         }
1209         return desiredLocation
1210     }
1211 
isTransitionRunningnull1212     private fun isTransitionRunning(): Boolean {
1213         return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
1214             animator.isRunning ||
1215             animationPending
1216     }
1217 
1218     @MediaLocation
calculateLocationnull1219     private fun calculateLocation(): Int {
1220         if (blockLocationChanges) {
1221             // Keep the current location until we're allowed to again
1222             return desiredLocation
1223         }
1224         val onLockscreen =
1225             (!bypassController.bypassEnabled && (statusbarState == StatusBarState.KEYGUARD))
1226 
1227         // UMO should show on hub unless the qs is expanding when not dreaming, or shade is
1228         // expanding when dreaming
1229         val onCommunal =
1230             (onCommunalNotDreaming && qsExpansion == 0.0f) || onCommunalDreamingAndShadeExpanding
1231         val location =
1232             when {
1233                 dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
1234                 onCommunal -> LOCATION_COMMUNAL_HUB
1235                 (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
1236                 qsExpansion > EXPANSION_THRESHOLD && onLockscreen -> LOCATION_QS
1237                 onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
1238                 onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
1239 
1240                 // Communal does not have its own StatusBarState so it should always have higher
1241                 // priority for the UMO over the lockscreen.
1242                 isCommunalShowing -> LOCATION_COMMUNAL_HUB
1243                 onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
1244                 else -> LOCATION_QQS
1245             }
1246         // When we're on lock screen and the player is not active, we should keep it in QS.
1247         // Otherwise it will try to animate a transition that doesn't make sense.
1248         if (
1249             location == LOCATION_LOCKSCREEN &&
1250                 getHost(location)?.visible != true &&
1251                 !statusBarStateController.isDozing
1252         ) {
1253             return LOCATION_QS
1254         }
1255         if (
1256             location == LOCATION_LOCKSCREEN &&
1257                 desiredLocation == LOCATION_QS &&
1258                 collapsingShadeFromQS
1259         ) {
1260             // When collapsing on the lockscreen, we want to remain in QS
1261             return LOCATION_QS
1262         }
1263         if (
1264             location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && !fullyAwake
1265         ) {
1266             // When unlocking from dozing / while waking up, the media shouldn't be transitioning
1267             // in an animated way. Let's keep it in the lockscreen until we're fully awake and
1268             // reattach it without an animation
1269             return LOCATION_LOCKSCREEN
1270         }
1271         // When communal showing while dreaming, skipQqsOnExpansion is also true but we want to
1272         // return the calculated location, so it won't disappear as soon as shade is pulled down.
1273         if (isCommunalShowing) return location
1274         if (skipQqsOnExpansion) {
1275             // When doing an immediate expand or collapse, we want to keep it in QS.
1276             return LOCATION_QS
1277         }
1278         return location
1279     }
1280 
isSplitShadeExpandingnull1281     private fun isSplitShadeExpanding(): Boolean {
1282         return inSplitShade && isTransitioningToFullShade
1283     }
1284 
1285     /** Are we currently transforming to the full shade and already in QQS */
isTransformingToFullShadeAndInQQSnull1286     private fun isTransformingToFullShadeAndInQQS(): Boolean {
1287         if (!isTransitioningToFullShade) {
1288             return false
1289         }
1290         if (inSplitShade) {
1291             // Split shade doesn't use QQS.
1292             return false
1293         }
1294         return fullShadeTransitionProgress > 0.5f
1295     }
1296 
1297     /** Is the current transformationType fading */
isCurrentlyFadingnull1298     private fun isCurrentlyFading(): Boolean {
1299         if (isSplitShadeExpanding()) {
1300             // Split shade always uses transition instead of fade.
1301             return false
1302         }
1303         if (isTransitioningToFullShade) {
1304             return true
1305         }
1306         return isCrossFadeAnimatorRunning
1307     }
1308 
1309     /** Update whether or not the media carousel could be visible to the user */
updateUserVisibilitynull1310     private fun updateUserVisibility() {
1311         val shadeVisible =
1312             isLockScreenVisibleToUser() ||
1313                 isLockScreenShadeVisibleToUser() ||
1314                 isHomeScreenShadeVisibleToUser() ||
1315                 isGlanceableHubVisibleToUser()
1316         val mediaVisible = qsExpanded || hasActiveMediaOrRecommendation
1317         mediaCarouselController.mediaCarouselScrollHandler.visibleToUser =
1318             shadeVisible && mediaVisible
1319     }
1320 
isLockScreenVisibleToUsernull1321     private fun isLockScreenVisibleToUser(): Boolean {
1322         return !statusBarStateController.isDozing &&
1323             !keyguardViewController.isBouncerShowing &&
1324             statusBarStateController.state == StatusBarState.KEYGUARD &&
1325             allowMediaPlayerOnLockScreen &&
1326             statusBarStateController.isExpanded &&
1327             !qsExpanded
1328     }
1329 
isLockScreenShadeVisibleToUsernull1330     private fun isLockScreenShadeVisibleToUser(): Boolean {
1331         return !statusBarStateController.isDozing &&
1332             !keyguardViewController.isBouncerShowing &&
1333             (statusBarStateController.state == StatusBarState.SHADE_LOCKED ||
1334                 (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded))
1335     }
1336 
isHomeScreenShadeVisibleToUsernull1337     private fun isHomeScreenShadeVisibleToUser(): Boolean {
1338         return !statusBarStateController.isDozing &&
1339             statusBarStateController.state == StatusBarState.SHADE &&
1340             statusBarStateController.isExpanded
1341     }
1342 
isGlanceableHubVisibleToUsernull1343     private fun isGlanceableHubVisibleToUser(): Boolean {
1344         return isCommunalShowing && !isPrimaryBouncerShowing && !isAnyShadeFullyExpanded
1345     }
1346 
1347     companion object {
1348         /** Attached in expanded quick settings */
1349         const val LOCATION_QS = 0
1350 
1351         /** Attached in the collapsed QS */
1352         const val LOCATION_QQS = 1
1353 
1354         /** Attached on the lock screen */
1355         const val LOCATION_LOCKSCREEN = 2
1356 
1357         /** Attached on the dream overlay */
1358         const val LOCATION_DREAM_OVERLAY = 3
1359 
1360         /** Attached to a view in the communal UI grid */
1361         const val LOCATION_COMMUNAL_HUB = 4
1362 
1363         /** Attached at the root of the hierarchy in an overlay */
1364         const val IN_OVERLAY = -1000
1365 
1366         /** Not attached to any view */
1367         const val LOCATION_UNKNOWN = -1
1368 
1369         /**
1370          * The default transformation type where the hosts transform into each other using a direct
1371          * transition
1372          */
1373         const val TRANSFORMATION_TYPE_TRANSITION = 0
1374 
1375         /**
1376          * A transformation type where content fades from one place to another instead of
1377          * transitioning
1378          */
1379         const val TRANSFORMATION_TYPE_FADE = 1
1380 
1381         /** Expansion amount value at which elements start to become visible in the QS panel. */
1382         const val EXPANSION_THRESHOLD = 0.4f
1383     }
1384 }
1385 
1386 private val EMPTY_RECT = Rect()
1387 
1388 @IntDef(
1389     prefix = ["TRANSFORMATION_TYPE_"],
1390     value =
1391         [
1392             MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION,
1393             MediaHierarchyManager.TRANSFORMATION_TYPE_FADE,
1394         ],
1395 )
1396 @Retention(AnnotationRetention.SOURCE)
1397 private annotation class TransformationType
1398 
1399 @IntDef(
1400     prefix = ["LOCATION_"],
1401     value =
1402         [
1403             MediaHierarchyManager.LOCATION_QS,
1404             MediaHierarchyManager.LOCATION_QQS,
1405             MediaHierarchyManager.LOCATION_LOCKSCREEN,
1406             MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
1407             MediaHierarchyManager.LOCATION_COMMUNAL_HUB,
1408             MediaHierarchyManager.LOCATION_UNKNOWN,
1409         ],
1410 )
1411 @Retention(AnnotationRetention.SOURCE)
1412 annotation class MediaLocation
1413