• 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 com.android.systemui.Interpolators
30 import com.android.systemui.keyguard.WakefulnessLifecycle
31 import com.android.systemui.plugins.statusbar.StatusBarStateController
32 import com.android.systemui.statusbar.NotificationLockscreenUserManager
33 import com.android.systemui.statusbar.StatusBarState
34 import com.android.systemui.statusbar.SysuiStatusBarStateController
35 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
36 import com.android.systemui.statusbar.phone.KeyguardBypassController
37 import com.android.systemui.statusbar.policy.KeyguardStateController
38 import com.android.systemui.util.animation.UniqueObjectHostView
39 import javax.inject.Inject
40 import javax.inject.Singleton
41 
42 /**
43  * Similarly to isShown but also excludes views that have 0 alpha
44  */
45 val View.isShownNotFaded: Boolean
46     get() {
47         var current: View = this
48         while (true) {
49             if (current.visibility != View.VISIBLE) {
50                 return false
51             }
52             if (current.alpha == 0.0f) {
53                 return false
54             }
55             val parent = current.parent ?: return false // We are not attached to the view root
56             if (parent !is View) {
57                 // we reached the viewroot, hurray
58                 return true
59             }
60             current = parent
61         }
62     }
63 
64 /**
65  * This manager is responsible for placement of the unique media view between the different hosts
66  * and animate the positions of the views to achieve seamless transitions.
67  */
68 @Singleton
69 class MediaHierarchyManager @Inject constructor(
70     private val context: Context,
71     private val statusBarStateController: SysuiStatusBarStateController,
72     private val keyguardStateController: KeyguardStateController,
73     private val bypassController: KeyguardBypassController,
74     private val mediaCarouselController: MediaCarouselController,
75     private val notifLockscreenUserManager: NotificationLockscreenUserManager,
76     wakefulnessLifecycle: WakefulnessLifecycle
77 ) {
78     /**
79      * The root overlay of the hierarchy. This is where the media notification is attached to
80      * whenever the view is transitioning from one host to another. It also make sure that the
81      * view is always in its final state when it is attached to a view host.
82      */
83     private var rootOverlay: ViewGroupOverlay? = null
84 
85     private var rootView: View? = null
86     private var currentBounds = Rect()
87     private var animationStartBounds: Rect = Rect()
88     private var targetBounds: Rect = Rect()
89     private val mediaFrame
90         get() = mediaCarouselController.mediaFrame
91     private var statusbarState: Int = statusBarStateController.state
<lambda>null92     private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
93         interpolator = Interpolators.FAST_OUT_SLOW_IN
94         addUpdateListener {
95             updateTargetState()
96             interpolateBounds(animationStartBounds, targetBounds, animatedFraction,
97                     result = currentBounds)
98             applyState(currentBounds)
99         }
100         addListener(object : AnimatorListenerAdapter() {
101             private var cancelled: Boolean = false
102 
103             override fun onAnimationCancel(animation: Animator?) {
104                 cancelled = true
105                 animationPending = false
106                 rootView?.removeCallbacks(startAnimation)
107             }
108 
109             override fun onAnimationEnd(animation: Animator?) {
110                 if (!cancelled) {
111                     applyTargetStateIfNotAnimating()
112                 }
113             }
114 
115             override fun onAnimationStart(animation: Animator?) {
116                 cancelled = false
117                 animationPending = false
118             }
119         })
120     }
121 
122     private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
123     /**
124      * The last location where this view was at before going to the desired location. This is
125      * useful for guided transitions.
126      */
127     @MediaLocation
128     private var previousLocation = -1
129     /**
130      * The desired location where the view will be at the end of the transition.
131      */
132     @MediaLocation
133     private var desiredLocation = -1
134 
135     /**
136      * The current attachment location where the view is currently attached.
137      * Usually this matches the desired location except for animations whenever a view moves
138      * to the new desired location, during which it is in [IN_OVERLAY].
139      */
140     @MediaLocation
141     private var currentAttachmentLocation = -1
142 
143     /**
144      * Are we currently waiting on an animation to start?
145      */
146     private var animationPending: Boolean = false
<lambda>null147     private val startAnimation: Runnable = Runnable { animator.start() }
148 
149     /**
150      * The expansion of quick settings
151      */
152     var qsExpansion: Float = 0.0f
153         set(value) {
154             if (field != value) {
155                 field = value
156                 updateDesiredLocation()
157                 if (getQSTransformationProgress() >= 0) {
158                     updateTargetState()
159                     applyTargetStateIfNotAnimating()
160                 }
161             }
162         }
163 
164     /**
165      * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
166      * we wouldn't want to transition in that case.
167      */
168     var collapsingShadeFromQS: Boolean = false
169         set(value) {
170             if (field != value) {
171                 field = value
172                 updateDesiredLocation(forceNoAnimation = true)
173             }
174         }
175 
176     /**
177      * Are location changes currently blocked?
178      */
179     private val blockLocationChanges: Boolean
180         get() {
181             return goingToSleep || dozeAnimationRunning
182         }
183 
184     /**
185      * Are we currently going to sleep
186      */
187     private var goingToSleep: Boolean = false
188         set(value) {
189             if (field != value) {
190                 field = value
191                 if (!value) {
192                     updateDesiredLocation()
193                 }
194             }
195         }
196 
197     /**
198      * Are we currently fullyAwake
199      */
200     private var fullyAwake: Boolean = false
201         set(value) {
202             if (field != value) {
203                 field = value
204                 if (value) {
205                     updateDesiredLocation(forceNoAnimation = true)
206                 }
207             }
208         }
209 
210     /**
211      * Is the doze animation currently Running
212      */
213     private var dozeAnimationRunning: Boolean = false
214         private set(value) {
215             if (field != value) {
216                 field = value
217                 if (!value) {
218                     updateDesiredLocation()
219                 }
220             }
221         }
222 
223     init {
224         statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
onStatePreChangenull225             override fun onStatePreChange(oldState: Int, newState: Int) {
226                 // We're updating the location before the state change happens, since we want the
227                 // location of the previous state to still be up to date when the animation starts
228                 statusbarState = newState
229                 updateDesiredLocation()
230             }
231 
onStateChangednull232             override fun onStateChanged(newState: Int) {
233                 updateTargetState()
234             }
235 
onDozeAmountChangednull236             override fun onDozeAmountChanged(linear: Float, eased: Float) {
237                 dozeAnimationRunning = linear != 0.0f && linear != 1.0f
238             }
239 
onDozingChangednull240             override fun onDozingChanged(isDozing: Boolean) {
241                 if (!isDozing) {
242                     dozeAnimationRunning = false
243                 } else {
244                     updateDesiredLocation()
245                 }
246             }
247         })
248 
249         wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
onFinishedGoingToSleepnull250             override fun onFinishedGoingToSleep() {
251                 goingToSleep = false
252             }
253 
onStartedGoingToSleepnull254             override fun onStartedGoingToSleep() {
255                 goingToSleep = true
256                 fullyAwake = false
257             }
258 
onFinishedWakingUpnull259             override fun onFinishedWakingUp() {
260                 goingToSleep = false
261                 fullyAwake = true
262             }
263 
onStartedWakingUpnull264             override fun onStartedWakingUp() {
265                 goingToSleep = false
266             }
267         })
268     }
269 
270     /**
271      * Register a media host and create a view can be attached to a view hierarchy
272      * and where the players will be placed in when the host is the currently desired state.
273      *
274      * @return the hostView associated with this location
275      */
registernull276     fun register(mediaObject: MediaHost): UniqueObjectHostView {
277         val viewHost = createUniqueObjectHost()
278         mediaObject.hostView = viewHost
279         mediaObject.addVisibilityChangeListener {
280             // Never animate because of a visibility change, only state changes should do that
281             updateDesiredLocation(forceNoAnimation = true)
282         }
283         mediaHosts[mediaObject.location] = mediaObject
284         if (mediaObject.location == desiredLocation) {
285             // In case we are overriding a view that is already visible, make sure we attach it
286             // to this new host view in the below call
287             desiredLocation = -1
288         }
289         if (mediaObject.location == currentAttachmentLocation) {
290             currentAttachmentLocation = -1
291         }
292         updateDesiredLocation()
293         return viewHost
294     }
295 
296     /**
297      * Close the guts in all players in [MediaCarouselController].
298      */
closeGutsnull299     fun closeGuts() {
300         mediaCarouselController.closeGuts()
301     }
302 
createUniqueObjectHostnull303     private fun createUniqueObjectHost(): UniqueObjectHostView {
304         val viewHost = UniqueObjectHostView(context)
305         viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
306             override fun onViewAttachedToWindow(p0: View?) {
307                 if (rootOverlay == null) {
308                     rootView = viewHost.viewRootImpl.view
309                     rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
310                 }
311                 viewHost.removeOnAttachStateChangeListener(this)
312             }
313 
314             override fun onViewDetachedFromWindow(p0: View?) {
315             }
316         })
317         return viewHost
318     }
319 
320     /**
321      * Updates the location that the view should be in. If it changes, an animation may be triggered
322      * going from the old desired location to the new one.
323      *
324      * @param forceNoAnimation optional parameter telling the system not to animate
325      */
updateDesiredLocationnull326     private fun updateDesiredLocation(forceNoAnimation: Boolean = false) {
327         val desiredLocation = calculateLocation()
328         if (desiredLocation != this.desiredLocation) {
329             if (this.desiredLocation >= 0) {
330                 previousLocation = this.desiredLocation
331             }
332             val isNewView = this.desiredLocation == -1
333             this.desiredLocation = desiredLocation
334             // Let's perform a transition
335             val animate = !forceNoAnimation &&
336                     shouldAnimateTransition(desiredLocation, previousLocation)
337             val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
338             val host = getHost(desiredLocation)
339             mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, animate,
340                     animDuration, delay)
341             performTransitionToNewLocation(isNewView, animate)
342         }
343     }
344 
performTransitionToNewLocationnull345     private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) {
346         if (previousLocation < 0 || isNewView) {
347             cancelAnimationAndApplyDesiredState()
348             return
349         }
350         val currentHost = getHost(desiredLocation)
351         val previousHost = getHost(previousLocation)
352         if (currentHost == null || previousHost == null) {
353             cancelAnimationAndApplyDesiredState()
354             return
355         }
356         updateTargetState()
357         if (isCurrentlyInGuidedTransformation()) {
358             applyTargetStateIfNotAnimating()
359         } else if (animate) {
360             animator.cancel()
361             if (currentAttachmentLocation != previousLocation ||
362                     !previousHost.hostView.isAttachedToWindow) {
363                 // Let's animate to the new position, starting from the current position
364                 // We also go in here in case the view was detached, since the bounds wouldn't
365                 // be correct anymore
366                 animationStartBounds.set(currentBounds)
367             } else {
368                 // otherwise, let's take the freshest state, since the current one could
369                 // be outdated
370                 animationStartBounds.set(previousHost.currentBounds)
371             }
372             adjustAnimatorForTransition(desiredLocation, previousLocation)
373             if (!animationPending) {
374                 rootView?.let {
375                     // Let's delay the animation start until we finished laying out
376                     animationPending = true
377                     it.postOnAnimation(startAnimation)
378                 }
379             }
380         } else {
381             cancelAnimationAndApplyDesiredState()
382         }
383     }
384 
shouldAnimateTransitionnull385     private fun shouldAnimateTransition(
386         @MediaLocation currentLocation: Int,
387         @MediaLocation previousLocation: Int
388     ): Boolean {
389         if (isCurrentlyInGuidedTransformation()) {
390             return false
391         }
392         // This is an invalid transition, and can happen when using the camera gesture from the
393         // lock screen. Disallow.
394         if (previousLocation == LOCATION_LOCKSCREEN &&
395             desiredLocation == LOCATION_QQS &&
396             statusbarState == StatusBarState.SHADE) {
397             return false
398         }
399 
400         if (currentLocation == LOCATION_QQS &&
401                 previousLocation == LOCATION_LOCKSCREEN &&
402                 (statusBarStateController.leaveOpenOnKeyguardHide() ||
403                         statusbarState == StatusBarState.SHADE_LOCKED)) {
404             // Usually listening to the isShown is enough to determine this, but there is some
405             // non-trivial reattaching logic happening that will make the view not-shown earlier
406             return true
407         }
408         return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
409     }
410 
adjustAnimatorForTransitionnull411     private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
412         val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
413         animator.apply {
414             duration = animDuration
415             startDelay = delay
416         }
417     }
418 
getAnimationParamsnull419     private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
420         var animDuration = 200L
421         var delay = 0L
422         if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
423             // Going to the full shade, let's adjust the animation duration
424             if (statusbarState == StatusBarState.SHADE &&
425                     keyguardStateController.isKeyguardFadingAway) {
426                 delay = keyguardStateController.keyguardFadingAwayDelay
427             }
428             animDuration = StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE.toLong()
429         } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
430             animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
431         }
432         return animDuration to delay
433     }
434 
applyTargetStateIfNotAnimatingnull435     private fun applyTargetStateIfNotAnimating() {
436         if (!animator.isRunning) {
437             // Let's immediately apply the target state (which is interpolated) if there is
438             // no animation running. Otherwise the animation update will already update
439             // the location
440             applyState(targetBounds)
441         }
442     }
443 
444     /**
445      * Updates the bounds that the view wants to be in at the end of the animation.
446      */
updateTargetStatenull447     private fun updateTargetState() {
448         if (isCurrentlyInGuidedTransformation()) {
449             val progress = getTransformationProgress()
450             var endHost = getHost(desiredLocation)!!
451             var starthost = getHost(previousLocation)!!
452             // If either of the hosts are invisible, let's keep them at the other host location to
453             // have a nicer disappear animation. Otherwise the currentBounds of the state might
454             // be undefined
455             if (!endHost.visible) {
456                 endHost = starthost
457             } else if (!starthost.visible) {
458                 starthost = endHost
459             }
460             val newBounds = endHost.currentBounds
461             val previousBounds = starthost.currentBounds
462             targetBounds = interpolateBounds(previousBounds, newBounds, progress)
463         } else {
464             val bounds = getHost(desiredLocation)?.currentBounds ?: return
465             targetBounds.set(bounds)
466         }
467     }
468 
interpolateBoundsnull469     private fun interpolateBounds(
470         startBounds: Rect,
471         endBounds: Rect,
472         progress: Float,
473         result: Rect? = null
474     ): Rect {
475         val left = MathUtils.lerp(startBounds.left.toFloat(),
476                 endBounds.left.toFloat(), progress).toInt()
477         val top = MathUtils.lerp(startBounds.top.toFloat(),
478                 endBounds.top.toFloat(), progress).toInt()
479         val right = MathUtils.lerp(startBounds.right.toFloat(),
480                 endBounds.right.toFloat(), progress).toInt()
481         val bottom = MathUtils.lerp(startBounds.bottom.toFloat(),
482                 endBounds.bottom.toFloat(), progress).toInt()
483         val resultBounds = result ?: Rect()
484         resultBounds.set(left, top, right, bottom)
485         return resultBounds
486     }
487 
488     /**
489      * @return true if this transformation is guided by an external progress like a finger
490      */
isCurrentlyInGuidedTransformationnull491     private fun isCurrentlyInGuidedTransformation(): Boolean {
492         return getTransformationProgress() >= 0
493     }
494 
495     /**
496      * @return the current transformation progress if we're in a guided transformation and -1
497      * otherwise
498      */
getTransformationProgressnull499     private fun getTransformationProgress(): Float {
500         val progress = getQSTransformationProgress()
501         if (progress >= 0) {
502             return progress
503         }
504         return -1.0f
505     }
506 
getQSTransformationProgressnull507     private fun getQSTransformationProgress(): Float {
508         val currentHost = getHost(desiredLocation)
509         val previousHost = getHost(previousLocation)
510         if (currentHost?.location == LOCATION_QS) {
511             if (previousHost?.location == LOCATION_QQS) {
512                 if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
513                     return qsExpansion
514                 }
515             }
516         }
517         return -1.0f
518     }
519 
getHostnull520     private fun getHost(@MediaLocation location: Int): MediaHost? {
521         if (location < 0) {
522             return null
523         }
524         return mediaHosts[location]
525     }
526 
cancelAnimationAndApplyDesiredStatenull527     private fun cancelAnimationAndApplyDesiredState() {
528         animator.cancel()
529         getHost(desiredLocation)?.let {
530             applyState(it.currentBounds, immediately = true)
531         }
532     }
533 
534     /**
535      * Apply the current state to the view, updating it's bounds and desired state
536      */
applyStatenull537     private fun applyState(bounds: Rect, immediately: Boolean = false) {
538         currentBounds.set(bounds)
539         val currentlyInGuidedTransformation = isCurrentlyInGuidedTransformation()
540         val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
541         val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
542         val endLocation = desiredLocation
543         mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately)
544         updateHostAttachment()
545         if (currentAttachmentLocation == IN_OVERLAY) {
546             mediaFrame.setLeftTopRightBottom(
547                     currentBounds.left,
548                     currentBounds.top,
549                     currentBounds.right,
550                     currentBounds.bottom)
551         }
552     }
553 
updateHostAttachmentnull554     private fun updateHostAttachment() {
555         val inOverlay = isTransitionRunning() && rootOverlay != null
556         val newLocation = if (inOverlay) IN_OVERLAY else desiredLocation
557         if (currentAttachmentLocation != newLocation) {
558             currentAttachmentLocation = newLocation
559 
560             // Remove the carousel from the old host
561             (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
562 
563             // Add it to the new one
564             val targetHost = getHost(desiredLocation)!!.hostView
565             if (inOverlay) {
566                 rootOverlay!!.add(mediaFrame)
567             } else {
568                 // When adding back to the host, let's make sure to reset the bounds.
569                 // Usually adding the view will trigger a layout that does this automatically,
570                 // but we sometimes suppress this.
571                 targetHost.addView(mediaFrame)
572                 val left = targetHost.paddingLeft
573                 val top = targetHost.paddingTop
574                 mediaFrame.setLeftTopRightBottom(
575                         left,
576                         top,
577                         left + currentBounds.width(),
578                         top + currentBounds.height())
579             }
580         }
581     }
582 
isTransitionRunningnull583     private fun isTransitionRunning(): Boolean {
584         return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
585                 animator.isRunning || animationPending
586     }
587 
588     @MediaLocation
calculateLocationnull589     private fun calculateLocation(): Int {
590         if (blockLocationChanges) {
591             // Keep the current location until we're allowed to again
592             return desiredLocation
593         }
594         val onLockscreen = (!bypassController.bypassEnabled &&
595                 (statusbarState == StatusBarState.KEYGUARD ||
596                         statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
597         val allowedOnLockscreen = notifLockscreenUserManager.shouldShowLockscreenNotifications()
598         val location = when {
599             qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS
600             qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
601             onLockscreen && allowedOnLockscreen -> LOCATION_LOCKSCREEN
602             else -> LOCATION_QQS
603         }
604         // When we're on lock screen and the player is not active, we should keep it in QS.
605         // Otherwise it will try to animate a transition that doesn't make sense.
606         if (location == LOCATION_LOCKSCREEN && getHost(location)?.visible != true &&
607                 !statusBarStateController.isDozing) {
608             return LOCATION_QS
609         }
610         if (location == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS &&
611                 collapsingShadeFromQS) {
612             // When collapsing on the lockscreen, we want to remain in QS
613             return LOCATION_QS
614         }
615         if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN &&
616                 !fullyAwake) {
617             // When unlocking from dozing / while waking up, the media shouldn't be transitioning
618             // in an animated way. Let's keep it in the lockscreen until we're fully awake and
619             // reattach it without an animation
620             return LOCATION_LOCKSCREEN
621         }
622         return location
623     }
624 
625     companion object {
626         /**
627          * Attached in expanded quick settings
628          */
629         const val LOCATION_QS = 0
630 
631         /**
632          * Attached in the collapsed QS
633          */
634         const val LOCATION_QQS = 1
635 
636         /**
637          * Attached on the lock screen
638          */
639         const val LOCATION_LOCKSCREEN = 2
640 
641         /**
642          * Attached at the root of the hierarchy in an overlay
643          */
644         const val IN_OVERLAY = -1000
645     }
646 }
647 
648 @IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS,
649     MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN])
650 @Retention(AnnotationRetention.SOURCE)
651 annotation class MediaLocation