1 /*
2  * 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
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.graphics.Rect
26 import android.util.MathUtils
27 import android.view.View
28 import android.view.ViewGroup
29 import android.view.ViewGroupOverlay
30 import androidx.annotation.VisibleForTesting
31 import com.android.systemui.R
32 import com.android.systemui.animation.Interpolators
33 import com.android.systemui.dagger.SysUISingleton
34 import com.android.systemui.keyguard.WakefulnessLifecycle
35 import com.android.systemui.plugins.statusbar.StatusBarStateController
36 import com.android.systemui.statusbar.CrossFadeHelper
37 import com.android.systemui.statusbar.NotificationLockscreenUserManager
38 import com.android.systemui.statusbar.StatusBarState
39 import com.android.systemui.statusbar.SysuiStatusBarStateController
40 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
41 import com.android.systemui.statusbar.phone.KeyguardBypassController
42 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
43 import com.android.systemui.statusbar.policy.ConfigurationController
44 import com.android.systemui.statusbar.policy.KeyguardStateController
45 import com.android.systemui.util.Utils
46 import com.android.systemui.util.animation.UniqueObjectHostView
47 import javax.inject.Inject
48 
49 /**
50  * Similarly to isShown but also excludes views that have 0 alpha
51  */
52 val View.isShownNotFaded: Boolean
53     get() {
54         var current: View = this
55         while (true) {
56             if (current.visibility != View.VISIBLE) {
57                 return false
58             }
59             if (current.alpha == 0.0f) {
60                 return false
61             }
62             val parent = current.parent ?: return false // We are not attached to the view root
63             if (parent !is View) {
64                 // we reached the viewroot, hurray
65                 return true
66             }
67             current = parent
68         }
69     }
70 
71 /**
72  * This manager is responsible for placement of the unique media view between the different hosts
73  * and animate the positions of the views to achieve seamless transitions.
74  */
75 @SysUISingleton
76 class MediaHierarchyManager @Inject constructor(
77     private val context: Context,
78     private val statusBarStateController: SysuiStatusBarStateController,
79     private val keyguardStateController: KeyguardStateController,
80     private val bypassController: KeyguardBypassController,
81     private val mediaCarouselController: MediaCarouselController,
82     private val notifLockscreenUserManager: NotificationLockscreenUserManager,
83     configurationController: ConfigurationController,
84     wakefulnessLifecycle: WakefulnessLifecycle,
85     private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager
86 ) {
87 
88     /**
89      * The root overlay of the hierarchy. This is where the media notification is attached to
90      * whenever the view is transitioning from one host to another. It also make sure that the
91      * view is always in its final state when it is attached to a view host.
92      */
93     private var rootOverlay: ViewGroupOverlay? = null
94 
95     private var rootView: View? = null
96     private var currentBounds = Rect()
97     private var animationStartBounds: Rect = Rect()
98 
99     /**
100      * The cross fade progress at the start of the animation. 0.5f means it's just switching between
101      * the start and the end location and the content is fully faded, while 0.75f means that we're
102      * halfway faded in again in the target state.
103      */
104     private var animationStartCrossFadeProgress = 0.0f
105 
106     /**
107      * The starting alpha of the animation
108      */
109     private var animationStartAlpha = 0.0f
110 
111     /**
112      * The starting location of the cross fade if an animation is running right now.
113      */
114     @MediaLocation
115     private var crossFadeAnimationStartLocation = -1
116 
117     /**
118      * The end location of the cross fade if an animation is running right now.
119      */
120     @MediaLocation
121     private var crossFadeAnimationEndLocation = -1
122     private var targetBounds: Rect = Rect()
123     private val mediaFrame
124         get() = mediaCarouselController.mediaFrame
125     private var statusbarState: Int = statusBarStateController.state
126     private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
127         interpolator = Interpolators.FAST_OUT_SLOW_IN
128         addUpdateListener {
129             updateTargetState()
130             val currentAlpha: Float
131             var boundsProgress = animatedFraction
132             if (isCrossFadeAnimatorRunning) {
133                 animationCrossFadeProgress = MathUtils.lerp(animationStartCrossFadeProgress, 1.0f,
134                     animatedFraction)
135                 // When crossfading, let's keep the bounds at the right location during fading
136                 boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f
137                 currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress,
138                     instantlyShowAtEnd = false)
139             } else {
140                 // If we're not crossfading, let's interpolate from the start alpha to 1.0f
141                 currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction)
142             }
143             interpolateBounds(animationStartBounds, targetBounds, boundsProgress,
144                     result = currentBounds)
145             applyState(currentBounds, currentAlpha)
146         }
147         addListener(object : AnimatorListenerAdapter() {
148             private var cancelled: Boolean = false
149 
150             override fun onAnimationCancel(animation: Animator?) {
151                 cancelled = true
152                 animationPending = false
153                 rootView?.removeCallbacks(startAnimation)
154             }
155 
156             override fun onAnimationEnd(animation: Animator?) {
157                 isCrossFadeAnimatorRunning = false
158                 if (!cancelled) {
159                     applyTargetStateIfNotAnimating()
160                 }
161             }
162 
163             override fun onAnimationStart(animation: Animator?) {
164                 cancelled = false
165                 animationPending = false
166             }
167         })
168     }
169 
170     private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
171     /**
172      * The last location where this view was at before going to the desired location. This is
173      * useful for guided transitions.
174      */
175     @MediaLocation
176     private var previousLocation = -1
177     /**
178      * The desired location where the view will be at the end of the transition.
179      */
180     @MediaLocation
181     private var desiredLocation = -1
182 
183     /**
184      * The current attachment location where the view is currently attached.
185      * Usually this matches the desired location except for animations whenever a view moves
186      * to the new desired location, during which it is in [IN_OVERLAY].
187      */
188     @MediaLocation
189     private var currentAttachmentLocation = -1
190 
191     private var inSplitShade = false
192 
193     /**
194      * Is there any active media in the carousel?
195      */
196     private var hasActiveMedia: Boolean = false
197         get() = mediaHosts.get(LOCATION_QQS)?.visible == true
198 
199     /**
200      * Are we currently waiting on an animation to start?
201      */
202     private var animationPending: Boolean = false
203     private val startAnimation: Runnable = Runnable { animator.start() }
204 
205     /**
206      * The expansion of quick settings
207      */
208     var qsExpansion: Float = 0.0f
209         set(value) {
210             if (field != value) {
211                 field = value
212                 updateDesiredLocation()
213                 if (getQSTransformationProgress() >= 0) {
214                     updateTargetState()
215                     applyTargetStateIfNotAnimating()
216                 }
217             }
218         }
219 
220     /**
221      * Is quick setting expanded?
222      */
223     var qsExpanded: Boolean = false
224         set(value) {
225             if (field != value) {
226                 field = value
227                 mediaCarouselController.mediaCarouselScrollHandler.qsExpanded = value
228             }
229             // qs is expanded on LS shade and HS shade
230             if (value && (isLockScreenShadeVisibleToUser() || isHomeScreenShadeVisibleToUser())) {
231                 mediaCarouselController.logSmartspaceImpression(value)
232             }
233             mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
234         }
235 
236     /**
237      * distance that the full shade transition takes in order for media to fully transition to the
238      * shade
239      */
240     private var distanceForFullShadeTransition = 0
241 
242     /**
243      * The amount of progress we are currently in if we're transitioning to the full shade.
244      * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
245      * shade.
246      */
247     private var fullShadeTransitionProgress = 0f
248         set(value) {
249             if (field == value) {
250                 return
251             }
252             field = value
253             if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) {
254                 // No need to do all the calculations / updates below if we're not on the lockscreen
255                 // or if we're bypassing.
256                 return
257             }
258             updateDesiredLocation(forceNoAnimation = isCurrentlyFading())
259             if (value >= 0) {
260                 updateTargetState()
261                 // Setting the alpha directly, as the below call will use it to update the alpha
262                 carouselAlpha = calculateAlphaFromCrossFade(field, instantlyShowAtEnd = true)
263                 applyTargetStateIfNotAnimating()
264             }
265         }
266 
267     /**
268      * Is there currently a cross-fade animation running driven by an animator?
269      */
270     private var isCrossFadeAnimatorRunning = false
271 
272     /**
273      * Are we currently transitionioning from the lockscreen to the full shade
274      * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and
275      * the transition starts, this will no longer return true.
276      */
277     private val isTransitioningToFullShade: Boolean
278         get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled &&
279             statusbarState == StatusBarState.KEYGUARD
280 
281     /**
282      * Set the amount of pixels we have currently dragged down if we're transitioning to the full
283      * shade. 0.0f means we're not transitioning yet.
284      */
285     fun setTransitionToFullShadeAmount(value: Float) {
286         // If we're transitioning starting on the shade_locked, we don't want any delay and rather
287         // have it aligned with the rest of the animation
288         val progress = MathUtils.saturate(value / distanceForFullShadeTransition)
289         fullShadeTransitionProgress = progress
290     }
291 
292     /**
293      * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
294      * we wouldn't want to transition in that case.
295      */
296     var collapsingShadeFromQS: Boolean = false
297         set(value) {
298             if (field != value) {
299                 field = value
300                 updateDesiredLocation(forceNoAnimation = true)
301             }
302         }
303 
304     /**
305      * Are location changes currently blocked?
306      */
307     private val blockLocationChanges: Boolean
308         get() {
309             return goingToSleep || dozeAnimationRunning
310         }
311 
312     /**
313      * Are we currently going to sleep
314      */
315     private var goingToSleep: Boolean = false
316         set(value) {
317             if (field != value) {
318                 field = value
319                 if (!value) {
320                     updateDesiredLocation()
321                 }
322             }
323         }
324 
325     /**
326      * Are we currently fullyAwake
327      */
328     private var fullyAwake: Boolean = false
329         set(value) {
330             if (field != value) {
331                 field = value
332                 if (value) {
333                     updateDesiredLocation(forceNoAnimation = true)
334                 }
335             }
336         }
337 
338     /**
339      * Is the doze animation currently Running
340      */
341     private var dozeAnimationRunning: Boolean = false
342         private set(value) {
343             if (field != value) {
344                 field = value
345                 if (!value) {
346                     updateDesiredLocation()
347                 }
348             }
349         }
350 
351     /**
352      * The current cross fade progress. 0.5f means it's just switching
353      * between the start and the end location and the content is fully faded, while 0.75f means
354      * that we're halfway faded in again in the target state.
355      * This is only valid while [isCrossFadeAnimatorRunning] is true.
356      */
357     private var animationCrossFadeProgress = 1.0f
358 
359     /**
360      * The current carousel Alpha.
361      */
362     private var carouselAlpha: Float = 1.0f
363         set(value) {
364             if (field == value) {
365                 return
366             }
367             field = value
368             CrossFadeHelper.fadeIn(mediaFrame, value)
369         }
370 
371     /**
372      * Calculate the alpha of the view when given a cross-fade progress.
373      *
374      * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching
375      * between the start and the end location and the content is fully faded, while 0.75f means
376      * that we're halfway faded in again in the target state.
377      *
378      * @param instantlyShowAtEnd should the view be instantly shown at the end. This is needed
379      * to avoid fadinging in when the target was hidden anyway.
380      */
381     private fun calculateAlphaFromCrossFade(
382         crossFadeProgress: Float,
383         instantlyShowAtEnd: Boolean
384     ): Float {
385         if (crossFadeProgress <= 0.5f) {
386             return 1.0f - crossFadeProgress / 0.5f
387         } else if (instantlyShowAtEnd) {
388             return 1.0f
389         } else {
390             return (crossFadeProgress - 0.5f) / 0.5f
391         }
392     }
393 
394     init {
395         updateConfiguration()
396         configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
397             override fun onConfigChanged(newConfig: Configuration?) {
398                 updateConfiguration()
399                 updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true)
400             }
401         })
402         statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
403             override fun onStatePreChange(oldState: Int, newState: Int) {
404                 // We're updating the location before the state change happens, since we want the
405                 // location of the previous state to still be up to date when the animation starts
406                 statusbarState = newState
407                 updateDesiredLocation()
408             }
409 
410             override fun onStateChanged(newState: Int) {
411                 updateTargetState()
412                 // Enters shade from lock screen
413                 if (newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser()) {
414                     mediaCarouselController.logSmartspaceImpression(qsExpanded)
415                 }
416                 mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
417             }
418 
419             override fun onDozeAmountChanged(linear: Float, eased: Float) {
420                 dozeAnimationRunning = linear != 0.0f && linear != 1.0f
421             }
422 
423             override fun onDozingChanged(isDozing: Boolean) {
424                 if (!isDozing) {
425                     dozeAnimationRunning = false
426                     // Enters lock screen from screen off
427                     if (isLockScreenVisibleToUser()) {
428                         mediaCarouselController.logSmartspaceImpression(qsExpanded)
429                     }
430                 } else {
431                     updateDesiredLocation()
432                     qsExpanded = false
433                     closeGuts()
434                 }
435                 mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
436             }
437 
438             override fun onExpandedChanged(isExpanded: Boolean) {
439                 // Enters shade from home screen
440                 if (isHomeScreenShadeVisibleToUser()) {
441                     mediaCarouselController.logSmartspaceImpression(qsExpanded)
442                 }
443                 mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
444             }
445         })
446 
447         wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
448             override fun onFinishedGoingToSleep() {
449                 goingToSleep = false
450             }
451 
452             override fun onStartedGoingToSleep() {
453                 goingToSleep = true
454                 fullyAwake = false
455             }
456 
457             override fun onFinishedWakingUp() {
458                 goingToSleep = false
459                 fullyAwake = true
460             }
461 
462             override fun onStartedWakingUp() {
463                 goingToSleep = false
464             }
465         })
466 
467         mediaCarouselController.updateUserVisibility = {
468             mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
469         }
470     }
471 
472     private fun updateConfiguration() {
473         distanceForFullShadeTransition = context.resources.getDimensionPixelSize(
474                 R.dimen.lockscreen_shade_media_transition_distance)
475         inSplitShade = Utils.shouldUseSplitNotificationShade(context.resources)
476     }
477 
478     /**
479      * Register a media host and create a view can be attached to a view hierarchy
480      * and where the players will be placed in when the host is the currently desired state.
481      *
482      * @return the hostView associated with this location
483      */
484     fun register(mediaObject: MediaHost): UniqueObjectHostView {
485         val viewHost = createUniqueObjectHost()
486         mediaObject.hostView = viewHost
487         mediaObject.addVisibilityChangeListener {
488             // If QQS changes visibility, we need to force an update to ensure the transition
489             // goes into the correct state
490             val stateUpdate = mediaObject.location == LOCATION_QQS
491 
492             // Never animate because of a visibility change, only state changes should do that
493             updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate)
494         }
495         mediaHosts[mediaObject.location] = mediaObject
496         if (mediaObject.location == desiredLocation) {
497             // In case we are overriding a view that is already visible, make sure we attach it
498             // to this new host view in the below call
499             desiredLocation = -1
500         }
501         if (mediaObject.location == currentAttachmentLocation) {
502             currentAttachmentLocation = -1
503         }
504         updateDesiredLocation()
505         return viewHost
506     }
507 
508     /**
509      * Close the guts in all players in [MediaCarouselController].
510      */
511     fun closeGuts() {
512         mediaCarouselController.closeGuts()
513     }
514 
515     private fun createUniqueObjectHost(): UniqueObjectHostView {
516         val viewHost = UniqueObjectHostView(context)
517         viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
518             override fun onViewAttachedToWindow(p0: View?) {
519                 if (rootOverlay == null) {
520                     rootView = viewHost.viewRootImpl.view
521                     rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
522                 }
523                 viewHost.removeOnAttachStateChangeListener(this)
524             }
525 
526             override fun onViewDetachedFromWindow(p0: View?) {
527             }
528         })
529         return viewHost
530     }
531 
532     /**
533      * Updates the location that the view should be in. If it changes, an animation may be triggered
534      * going from the old desired location to the new one.
535      *
536      * @param forceNoAnimation optional parameter telling the system not to animate
537      * @param forceStateUpdate optional parameter telling the system to update transition state
538      *                         even if location did not change
539      */
540     private fun updateDesiredLocation(
541         forceNoAnimation: Boolean = false,
542         forceStateUpdate: Boolean = false
543     ) {
544         val desiredLocation = calculateLocation()
545         if (desiredLocation != this.desiredLocation || forceStateUpdate) {
546             if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) {
547                 // Only update previous location when it actually changes
548                 previousLocation = this.desiredLocation
549             } else if (forceStateUpdate) {
550                 val onLockscreen = (!bypassController.bypassEnabled &&
551                         (statusbarState == StatusBarState.KEYGUARD ||
552                             statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
553                 if (desiredLocation == LOCATION_QS && previousLocation == LOCATION_LOCKSCREEN &&
554                         !onLockscreen) {
555                     // If media active state changed and the device is now unlocked, update the
556                     // previous location so we animate between the correct hosts
557                     previousLocation = LOCATION_QQS
558                 }
559             }
560             val isNewView = this.desiredLocation == -1
561             this.desiredLocation = desiredLocation
562             // Let's perform a transition
563             val animate = !forceNoAnimation &&
564                     shouldAnimateTransition(desiredLocation, previousLocation)
565             val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
566             val host = getHost(desiredLocation)
567             val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE
568             if (!willFade || isCurrentlyInGuidedTransformation() || !animate) {
569                 // if we're fading, we want the desired location / measurement only to change
570                 // once fully faded. This is happening in the host attachment
571                 mediaCarouselController.onDesiredLocationChanged(desiredLocation, host,
572                     animate, animDuration, delay)
573             }
574             performTransitionToNewLocation(isNewView, animate)
575         }
576     }
577 
578     private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) {
579         if (previousLocation < 0 || isNewView) {
580             cancelAnimationAndApplyDesiredState()
581             return
582         }
583         val currentHost = getHost(desiredLocation)
584         val previousHost = getHost(previousLocation)
585         if (currentHost == null || previousHost == null) {
586             cancelAnimationAndApplyDesiredState()
587             return
588         }
589         updateTargetState()
590         if (isCurrentlyInGuidedTransformation()) {
591             applyTargetStateIfNotAnimating()
592         } else if (animate) {
593             val wasCrossFading = isCrossFadeAnimatorRunning
594             val previewsCrossFadeProgress = animationCrossFadeProgress
595             animator.cancel()
596             if (currentAttachmentLocation != previousLocation ||
597                     !previousHost.hostView.isAttachedToWindow) {
598                 // Let's animate to the new position, starting from the current position
599                 // We also go in here in case the view was detached, since the bounds wouldn't
600                 // be correct anymore
601                 animationStartBounds.set(currentBounds)
602             } else {
603                 // otherwise, let's take the freshest state, since the current one could
604                 // be outdated
605                 animationStartBounds.set(previousHost.currentBounds)
606             }
607             val transformationType = calculateTransformationType()
608             var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE
609             var crossFadeStartProgress = 0.0f
610             // The alpha is only relevant when not cross fading
611             var newCrossFadeStartLocation = previousLocation
612             if (wasCrossFading) {
613                 if (currentAttachmentLocation == crossFadeAnimationEndLocation) {
614                     if (needsCrossFade) {
615                         // We were previously crossFading and we've already reached
616                         // the end view, Let's start crossfading from the same position there
617                         crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
618                     }
619                     // Otherwise let's fade in from the current alpha, but not cross fade
620                 } else {
621                     // We haven't reached the previous location yet, let's still cross fade from
622                     // where we were.
623                     newCrossFadeStartLocation = crossFadeAnimationStartLocation
624                     if (newCrossFadeStartLocation == desiredLocation) {
625                         // we're crossFading back to where we were, let's start at the end position
626                         crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
627                     } else {
628                         // Let's start from where we are right now
629                         crossFadeStartProgress = previewsCrossFadeProgress
630                         // We need to force cross fading as we haven't reached the end location yet
631                         needsCrossFade = true
632                     }
633                 }
634             } else if (needsCrossFade) {
635                 // let's not flicker and start with the same alpha
636                 crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f
637             }
638             isCrossFadeAnimatorRunning = needsCrossFade
639             crossFadeAnimationStartLocation = newCrossFadeStartLocation
640             crossFadeAnimationEndLocation = desiredLocation
641             animationStartAlpha = carouselAlpha
642             animationStartCrossFadeProgress = crossFadeStartProgress
643             adjustAnimatorForTransition(desiredLocation, previousLocation)
644             if (!animationPending) {
645                 rootView?.let {
646                     // Let's delay the animation start until we finished laying out
647                     animationPending = true
648                     it.postOnAnimation(startAnimation)
649                 }
650             }
651         } else {
652             cancelAnimationAndApplyDesiredState()
653         }
654     }
655 
656     private fun shouldAnimateTransition(
657         @MediaLocation currentLocation: Int,
658         @MediaLocation previousLocation: Int
659     ): Boolean {
660         if (isCurrentlyInGuidedTransformation()) {
661             return false
662         }
663         // This is an invalid transition, and can happen when using the camera gesture from the
664         // lock screen. Disallow.
665         if (previousLocation == LOCATION_LOCKSCREEN &&
666             desiredLocation == LOCATION_QQS &&
667             statusbarState == StatusBarState.SHADE) {
668             return false
669         }
670 
671         if (currentLocation == LOCATION_QQS &&
672                 previousLocation == LOCATION_LOCKSCREEN &&
673                 (statusBarStateController.leaveOpenOnKeyguardHide() ||
674                         statusbarState == StatusBarState.SHADE_LOCKED)) {
675             // Usually listening to the isShown is enough to determine this, but there is some
676             // non-trivial reattaching logic happening that will make the view not-shown earlier
677             return true
678         }
679 
680         if (statusbarState == StatusBarState.KEYGUARD && (currentLocation == LOCATION_LOCKSCREEN ||
681                         previousLocation == LOCATION_LOCKSCREEN)) {
682             // We're always fading from lockscreen to keyguard in situations where the player
683             // is already fully hidden
684             return false
685         }
686         return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
687     }
688 
689     private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
690         val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
691         animator.apply {
692             duration = animDuration
693             startDelay = delay
694         }
695     }
696 
697     private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
698         var animDuration = 200L
699         var delay = 0L
700         if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
701             // Going to the full shade, let's adjust the animation duration
702             if (statusbarState == StatusBarState.SHADE &&
703                     keyguardStateController.isKeyguardFadingAway) {
704                 delay = keyguardStateController.keyguardFadingAwayDelay
705             }
706             animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong()
707         } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
708             animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
709         }
710         return animDuration to delay
711     }
712 
713     private fun applyTargetStateIfNotAnimating() {
714         if (!animator.isRunning) {
715             // Let's immediately apply the target state (which is interpolated) if there is
716             // no animation running. Otherwise the animation update will already update
717             // the location
718             applyState(targetBounds, carouselAlpha)
719         }
720     }
721 
722     /**
723      * Updates the bounds that the view wants to be in at the end of the animation.
724      */
725     private fun updateTargetState() {
726         if (isCurrentlyInGuidedTransformation() && !isCurrentlyFading()) {
727             val progress = getTransformationProgress()
728             var endHost = getHost(desiredLocation)!!
729             var starthost = getHost(previousLocation)!!
730             // If either of the hosts are invisible, let's keep them at the other host location to
731             // have a nicer disappear animation. Otherwise the currentBounds of the state might
732             // be undefined
733             if (!endHost.visible) {
734                 endHost = starthost
735             } else if (!starthost.visible) {
736                 starthost = endHost
737             }
738             val newBounds = endHost.currentBounds
739             val previousBounds = starthost.currentBounds
740             targetBounds = interpolateBounds(previousBounds, newBounds, progress)
741         } else {
742             val bounds = getHost(desiredLocation)?.currentBounds ?: return
743             targetBounds.set(bounds)
744         }
745     }
746 
747     private fun interpolateBounds(
748         startBounds: Rect,
749         endBounds: Rect,
750         progress: Float,
751         result: Rect? = null
752     ): Rect {
753         val left = MathUtils.lerp(startBounds.left.toFloat(),
754                 endBounds.left.toFloat(), progress).toInt()
755         val top = MathUtils.lerp(startBounds.top.toFloat(),
756                 endBounds.top.toFloat(), progress).toInt()
757         val right = MathUtils.lerp(startBounds.right.toFloat(),
758                 endBounds.right.toFloat(), progress).toInt()
759         val bottom = MathUtils.lerp(startBounds.bottom.toFloat(),
760                 endBounds.bottom.toFloat(), progress).toInt()
761         val resultBounds = result ?: Rect()
762         resultBounds.set(left, top, right, bottom)
763         return resultBounds
764     }
765 
766     /**
767      * @return true if this transformation is guided by an external progress like a finger
768      */
769     private fun isCurrentlyInGuidedTransformation(): Boolean {
770         return getTransformationProgress() >= 0
771     }
772 
773     /**
774      * Calculate the transformation type for the current animation
775      */
776     @VisibleForTesting
777     @TransformationType
778     fun calculateTransformationType(): Int {
779         if (isTransitioningToFullShade) {
780             return TRANSFORMATION_TYPE_FADE
781         }
782         if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS ||
783             previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN) {
784             // animating between ls and qs should fade, as QS is clipped.
785             return TRANSFORMATION_TYPE_FADE
786         }
787         if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
788             // animating between ls and qqs should fade when dragging down via e.g. expand button
789             return TRANSFORMATION_TYPE_FADE
790         }
791         return TRANSFORMATION_TYPE_TRANSITION
792     }
793 
794     /**
795      * @return the current transformation progress if we're in a guided transformation and -1
796      * otherwise
797      */
798     private fun getTransformationProgress(): Float {
799         val progress = getQSTransformationProgress()
800         if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
801             return progress
802         }
803         if (isTransitioningToFullShade) {
804             return fullShadeTransitionProgress
805         }
806         return -1.0f
807     }
808 
809     private fun getQSTransformationProgress(): Float {
810         val currentHost = getHost(desiredLocation)
811         val previousHost = getHost(previousLocation)
812         if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) {
813             if (previousHost?.location == LOCATION_QQS) {
814                 if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
815                     return qsExpansion
816                 }
817             }
818         }
819         return -1.0f
820     }
821 
822     private fun getHost(@MediaLocation location: Int): MediaHost? {
823         if (location < 0) {
824             return null
825         }
826         return mediaHosts[location]
827     }
828 
829     private fun cancelAnimationAndApplyDesiredState() {
830         animator.cancel()
831         getHost(desiredLocation)?.let {
832             applyState(it.currentBounds, alpha = 1.0f, immediately = true)
833         }
834     }
835 
836     /**
837      * Apply the current state to the view, updating it's bounds and desired state
838      */
839     private fun applyState(bounds: Rect, alpha: Float, immediately: Boolean = false) {
840         currentBounds.set(bounds)
841         carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f
842         val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading()
843         val startLocation = if (onlyUseEndState) -1 else previousLocation
844         val progress = if (onlyUseEndState) 1.0f else getTransformationProgress()
845         val endLocation = resolveLocationForFading()
846         mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately)
847         updateHostAttachment()
848         if (currentAttachmentLocation == IN_OVERLAY) {
849             mediaFrame.setLeftTopRightBottom(
850                     currentBounds.left,
851                     currentBounds.top,
852                     currentBounds.right,
853                     currentBounds.bottom)
854         }
855     }
856 
857     private fun updateHostAttachment() {
858         var newLocation = resolveLocationForFading()
859         var canUseOverlay = !isCurrentlyFading()
860         if (isCrossFadeAnimatorRunning) {
861             if (getHost(newLocation)?.visible == true &&
862                 getHost(newLocation)?.hostView?.isShown == false &&
863                 newLocation != desiredLocation) {
864                 // We're crossfading but the view is already hidden. Let's move to the overlay
865                 // instead. This happens when animating to the full shade using a button click.
866                 canUseOverlay = true
867             }
868         }
869         val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay
870         newLocation = if (inOverlay) IN_OVERLAY else newLocation
871         if (currentAttachmentLocation != newLocation) {
872             currentAttachmentLocation = newLocation
873 
874             // Remove the carousel from the old host
875             (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
876 
877             // Add it to the new one
878             if (inOverlay) {
879                 rootOverlay!!.add(mediaFrame)
880             } else {
881                 val targetHost = getHost(newLocation)!!.hostView
882                 // When adding back to the host, let's make sure to reset the bounds.
883                 // Usually adding the view will trigger a layout that does this automatically,
884                 // but we sometimes suppress this.
885                 targetHost.addView(mediaFrame)
886                 val left = targetHost.paddingLeft
887                 val top = targetHost.paddingTop
888                 mediaFrame.setLeftTopRightBottom(
889                         left,
890                         top,
891                         left + currentBounds.width(),
892                         top + currentBounds.height())
893             }
894             if (isCrossFadeAnimatorRunning) {
895                 // When cross-fading with an animation, we only notify the media carousel of the
896                 // location change, once the view is reattached to the new place and not immediately
897                 // when the desired location changes. This callback will update the measurement
898                 // of the carousel, only once we've faded out at the old location and then reattach
899                 // to fade it in at the new location.
900                 mediaCarouselController.onDesiredLocationChanged(
901                     newLocation,
902                     getHost(newLocation),
903                     animate = false
904                 )
905             }
906         }
907     }
908 
909     /**
910      * Calculate the location when cross fading between locations. While fading out,
911      * the content should remain in the previous location, while after the switch it should
912      * be at the desired location.
913      */
914     private fun resolveLocationForFading(): Int {
915         if (isCrossFadeAnimatorRunning) {
916             // When animating between two hosts with a fade, let's keep ourselves in the old
917             // location for the first half, and then switch over to the end location
918             if (animationCrossFadeProgress > 0.5 || previousLocation == -1) {
919                 return crossFadeAnimationEndLocation
920             } else {
921                 return crossFadeAnimationStartLocation
922             }
923         }
924         return desiredLocation
925     }
926 
927     private fun isTransitionRunning(): Boolean {
928         return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
929                 animator.isRunning || animationPending
930     }
931 
932     @MediaLocation
933     private fun calculateLocation(): Int {
934         if (blockLocationChanges) {
935             // Keep the current location until we're allowed to again
936             return desiredLocation
937         }
938         val onLockscreen = (!bypassController.bypassEnabled &&
939             (statusbarState == StatusBarState.KEYGUARD ||
940                 statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
941         val allowedOnLockscreen = notifLockscreenUserManager.shouldShowLockscreenNotifications()
942         val location = when {
943             (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
944             qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
945             !hasActiveMedia -> LOCATION_QS
946             onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
947             onLockscreen && allowedOnLockscreen -> LOCATION_LOCKSCREEN
948             else -> LOCATION_QQS
949         }
950         // When we're on lock screen and the player is not active, we should keep it in QS.
951         // Otherwise it will try to animate a transition that doesn't make sense.
952         if (location == LOCATION_LOCKSCREEN && getHost(location)?.visible != true &&
953             !statusBarStateController.isDozing) {
954             return LOCATION_QS
955         }
956         if (location == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS &&
957             collapsingShadeFromQS) {
958             // When collapsing on the lockscreen, we want to remain in QS
959             return LOCATION_QS
960         }
961         if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN &&
962             !fullyAwake) {
963             // When unlocking from dozing / while waking up, the media shouldn't be transitioning
964             // in an animated way. Let's keep it in the lockscreen until we're fully awake and
965             // reattach it without an animation
966             return LOCATION_LOCKSCREEN
967         }
968         return location
969     }
970 
971     /**
972      * Are we currently transforming to the full shade and already in QQS
973      */
974     private fun isTransformingToFullShadeAndInQQS(): Boolean {
975         if (!isTransitioningToFullShade) {
976             return false
977         }
978         return fullShadeTransitionProgress > 0.5f
979     }
980 
981     /**
982      * Is the current transformationType fading
983      */
984     private fun isCurrentlyFading(): Boolean {
985         if (isTransitioningToFullShade) {
986             return true
987         }
988         return isCrossFadeAnimatorRunning
989     }
990 
991     /**
992      * Returns true when the media card could be visible to the user if existed.
993      */
994     private fun isVisibleToUser(): Boolean {
995         return isLockScreenVisibleToUser() || isLockScreenShadeVisibleToUser() ||
996                 isHomeScreenShadeVisibleToUser()
997     }
998 
999     private fun isLockScreenVisibleToUser(): Boolean {
1000         return !statusBarStateController.isDozing &&
1001                 !statusBarKeyguardViewManager.isBouncerShowing &&
1002                 statusBarStateController.state == StatusBarState.KEYGUARD &&
1003                 notifLockscreenUserManager.shouldShowLockscreenNotifications() &&
1004                 statusBarStateController.isExpanded &&
1005                 !qsExpanded
1006     }
1007 
1008     private fun isLockScreenShadeVisibleToUser(): Boolean {
1009         return !statusBarStateController.isDozing &&
1010                 !statusBarKeyguardViewManager.isBouncerShowing &&
1011                 (statusBarStateController.state == StatusBarState.SHADE_LOCKED ||
1012                         (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded))
1013     }
1014 
1015     private fun isHomeScreenShadeVisibleToUser(): Boolean {
1016         return !statusBarStateController.isDozing &&
1017                 statusBarStateController.state == StatusBarState.SHADE &&
1018                 statusBarStateController.isExpanded
1019     }
1020 
1021     companion object {
1022         /**
1023          * Attached in expanded quick settings
1024          */
1025         const val LOCATION_QS = 0
1026 
1027         /**
1028          * Attached in the collapsed QS
1029          */
1030         const val LOCATION_QQS = 1
1031 
1032         /**
1033          * Attached on the lock screen
1034          */
1035         const val LOCATION_LOCKSCREEN = 2
1036 
1037         /**
1038          * Attached at the root of the hierarchy in an overlay
1039          */
1040         const val IN_OVERLAY = -1000
1041 
1042         /**
1043          * The default transformation type where the hosts transform into each other using a direct
1044          * transition
1045          */
1046         const val TRANSFORMATION_TYPE_TRANSITION = 0
1047 
1048         /**
1049          * A transformation type where content fades from one place to another instead of
1050          * transitioning
1051          */
1052         const val TRANSFORMATION_TYPE_FADE = 1
1053     }
1054 }
1055 
1056 @IntDef(prefix = ["TRANSFORMATION_TYPE_"], value = [
1057     MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION,
1058     MediaHierarchyManager.TRANSFORMATION_TYPE_FADE])
1059 @Retention(AnnotationRetention.SOURCE)
1060 private annotation class TransformationType
1061 
1062 @IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS,
1063     MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN])
1064 @Retention(AnnotationRetention.SOURCE)
1065 annotation class MediaLocation