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