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