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