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