blob: c41e6104833e84b12eef341d975f322725935e90 [file] [log] [blame]
Selim Cinek5dbef2d2020-05-07 17:44:38 -07001/*
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
17package com.android.systemui.media
18
Selim Cinekf0f74952020-04-21 11:45:16 -070019import android.animation.Animator
20import android.animation.AnimatorListenerAdapter
21import android.animation.ValueAnimator
Selim Cinek5dbef2d2020-05-07 17:44:38 -070022import android.annotation.IntDef
23import android.content.Context
Selim Cinek2de5ebb2020-05-20 15:39:03 -070024import android.graphics.Rect
25import android.util.MathUtils
Selim Cinekf0f74952020-04-21 11:45:16 -070026import android.view.View
Selim Cinek5dbef2d2020-05-07 17:44:38 -070027import android.view.ViewGroup
Selim Cinekf0f74952020-04-21 11:45:16 -070028import android.view.ViewGroupOverlay
Selim Cinekf0f74952020-04-21 11:45:16 -070029import com.android.systemui.Interpolators
Selim Cinek7f657602020-05-21 12:37:14 -070030import com.android.systemui.keyguard.WakefulnessLifecycle
Selim Cinek5dbef2d2020-05-07 17:44:38 -070031import com.android.systemui.plugins.statusbar.StatusBarStateController
Lucas Dupin989a1112020-05-19 18:56:28 -070032import com.android.systemui.statusbar.NotificationLockscreenUserManager
Selim Cinek5dbef2d2020-05-07 17:44:38 -070033import com.android.systemui.statusbar.StatusBarState
Selim Cinekf0f74952020-04-21 11:45:16 -070034import com.android.systemui.statusbar.SysuiStatusBarStateController
Selim Cinekf0f74952020-04-21 11:45:16 -070035import com.android.systemui.statusbar.notification.stack.StackStateAnimator
Selim Cinek5dbef2d2020-05-07 17:44:38 -070036import com.android.systemui.statusbar.phone.KeyguardBypassController
Selim Cinekf0f74952020-04-21 11:45:16 -070037import com.android.systemui.statusbar.policy.KeyguardStateController
Selim Cinek54809622020-04-30 19:04:44 -070038import com.android.systemui.util.animation.UniqueObjectHostView
Selim Cinek5dbef2d2020-05-07 17:44:38 -070039import javax.inject.Inject
40import javax.inject.Singleton
41
42/**
43 * This manager is responsible for placement of the unique media view between the different hosts
44 * and animate the positions of the views to achieve seamless transitions.
45 */
46@Singleton
47class MediaHierarchyManager @Inject constructor(
48 private val context: Context,
Selim Cinekf0f74952020-04-21 11:45:16 -070049 private val statusBarStateController: SysuiStatusBarStateController,
50 private val keyguardStateController: KeyguardStateController,
Selim Cinek5dbef2d2020-05-07 17:44:38 -070051 private val bypassController: KeyguardBypassController,
Selim Cinekafae4e72020-06-16 18:21:41 -070052 private val mediaCarouselController: MediaCarouselController,
Selim Cinek7f657602020-05-21 12:37:14 -070053 private val notifLockscreenUserManager: NotificationLockscreenUserManager,
54 wakefulnessLifecycle: WakefulnessLifecycle
Selim Cinek5dbef2d2020-05-07 17:44:38 -070055) {
Selim Cinekf418bb02020-05-04 17:16:58 -070056 /**
57 * The root overlay of the hierarchy. This is where the media notification is attached to
58 * whenever the view is transitioning from one host to another. It also make sure that the
59 * view is always in its final state when it is attached to a view host.
60 */
Selim Cinekf0f74952020-04-21 11:45:16 -070061 private var rootOverlay: ViewGroupOverlay? = null
Selim Cinek2de5ebb2020-05-20 15:39:03 -070062
63 private var rootView: View? = null
64 private var currentBounds = Rect()
65 private var animationStartBounds: Rect = Rect()
66 private var targetBounds: Rect = Rect()
67 private val mediaFrame
Selim Cinekafae4e72020-06-16 18:21:41 -070068 get() = mediaCarouselController.mediaFrame
Selim Cinek2d7be5f2020-05-01 13:16:01 -070069 private var statusbarState: Int = statusBarStateController.state
Selim Cinekf0f74952020-04-21 11:45:16 -070070 private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
71 interpolator = Interpolators.FAST_OUT_SLOW_IN
72 addUpdateListener {
73 updateTargetState()
Selim Cinek2de5ebb2020-05-20 15:39:03 -070074 interpolateBounds(animationStartBounds, targetBounds, animatedFraction,
75 result = currentBounds)
76 applyState(currentBounds)
Selim Cinekf0f74952020-04-21 11:45:16 -070077 }
78 addListener(object : AnimatorListenerAdapter() {
79 private var cancelled: Boolean = false
80
81 override fun onAnimationCancel(animation: Animator?) {
82 cancelled = true
Selim Cinek2de5ebb2020-05-20 15:39:03 -070083 animationPending = false
84 rootView?.removeCallbacks(startAnimation)
Selim Cinekf0f74952020-04-21 11:45:16 -070085 }
Selim Cinek2de5ebb2020-05-20 15:39:03 -070086
Selim Cinekf0f74952020-04-21 11:45:16 -070087 override fun onAnimationEnd(animation: Animator?) {
88 if (!cancelled) {
89 applyTargetStateIfNotAnimating()
90 }
91 }
92
93 override fun onAnimationStart(animation: Animator?) {
94 cancelled = false
Selim Cinek2de5ebb2020-05-20 15:39:03 -070095 animationPending = false
Selim Cinekf0f74952020-04-21 11:45:16 -070096 }
97 })
98 }
Selim Cinekf418bb02020-05-04 17:16:58 -070099
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700100 private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
Selim Cinekf418bb02020-05-04 17:16:58 -0700101 /**
102 * The last location where this view was at before going to the desired location. This is
103 * useful for guided transitions.
104 */
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700105 @MediaLocation
106 private var previousLocation = -1
Selim Cinekf418bb02020-05-04 17:16:58 -0700107 /**
108 * The desired location where the view will be at the end of the transition.
109 */
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700110 @MediaLocation
111 private var desiredLocation = -1
Selim Cinekf418bb02020-05-04 17:16:58 -0700112
113 /**
114 * The current attachment location where the view is currently attached.
115 * Usually this matches the desired location except for animations whenever a view moves
116 * to the new desired location, during which it is in [IN_OVERLAY].
117 */
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700118 @MediaLocation
119 private var currentAttachmentLocation = -1
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700120
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700121 /**
122 * Are we currently waiting on an animation to start?
123 */
124 private var animationPending: Boolean = false
125 private val startAnimation: Runnable = Runnable { animator.start() }
126
127 /**
128 * The expansion of quick settings
129 */
Selim Cinekf0f74952020-04-21 11:45:16 -0700130 var qsExpansion: Float = 0.0f
131 set(value) {
132 if (field != value) {
133 field = value
134 updateDesiredLocation()
135 if (getQSTransformationProgress() >= 0) {
136 updateTargetState()
137 applyTargetStateIfNotAnimating()
138 }
139 }
140 }
141
Selim Cinek7f657602020-05-21 12:37:14 -0700142 /**
Selim Cinekf630a822020-06-22 11:26:09 -0700143 * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
144 * we wouldn't want to transition in that case.
145 */
146 var collapsingShadeFromQS: Boolean = false
147 set(value) {
148 if (field != value) {
149 field = value
150 updateDesiredLocation(forceNoAnimation = true)
151 }
152 }
153
154 /**
Selim Cinek7f657602020-05-21 12:37:14 -0700155 * Are location changes currently blocked?
156 */
157 private val blockLocationChanges: Boolean
158 get() {
159 return goingToSleep || dozeAnimationRunning
160 }
161
162 /**
163 * Are we currently going to sleep
164 */
165 private var goingToSleep: Boolean = false
166 set(value) {
167 if (field != value) {
168 field = value
169 if (!value) {
170 updateDesiredLocation()
171 }
172 }
173 }
174
175 /**
Selim Cinekf630a822020-06-22 11:26:09 -0700176 * Are we currently fullyAwake
177 */
178 private var fullyAwake: Boolean = false
179 set(value) {
180 if (field != value) {
181 field = value
182 if (value) {
183 updateDesiredLocation(forceNoAnimation = true)
184 }
185 }
186 }
187
188 /**
Selim Cinek7f657602020-05-21 12:37:14 -0700189 * Is the doze animation currently Running
190 */
191 private var dozeAnimationRunning: Boolean = false
192 private set(value) {
193 if (field != value) {
194 field = value
195 if (!value) {
196 updateDesiredLocation()
197 }
198 }
199 }
200
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700201 init {
Selim Cinekf0f74952020-04-21 11:45:16 -0700202 statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
Selim Cinek2d7be5f2020-05-01 13:16:01 -0700203 override fun onStatePreChange(oldState: Int, newState: Int) {
204 // We're updating the location before the state change happens, since we want the
205 // location of the previous state to still be up to date when the animation starts
206 statusbarState = newState
Selim Cinekf0f74952020-04-21 11:45:16 -0700207 updateDesiredLocation()
208 }
Selim Cinek2d7be5f2020-05-01 13:16:01 -0700209
210 override fun onStateChanged(newState: Int) {
211 updateTargetState()
212 }
Selim Cinek7f657602020-05-21 12:37:14 -0700213
214 override fun onDozeAmountChanged(linear: Float, eased: Float) {
215 dozeAnimationRunning = linear != 0.0f && linear != 1.0f
216 }
217
218 override fun onDozingChanged(isDozing: Boolean) {
219 if (!isDozing) {
220 dozeAnimationRunning = false
Lucas Dupin7d52dd62020-06-18 18:11:22 -0700221 } else {
222 updateDesiredLocation()
Selim Cinek7f657602020-05-21 12:37:14 -0700223 }
224 }
225 })
226
227 wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
228 override fun onFinishedGoingToSleep() {
229 goingToSleep = false
230 }
231
232 override fun onStartedGoingToSleep() {
233 goingToSleep = true
Selim Cinekf630a822020-06-22 11:26:09 -0700234 fullyAwake = false
Selim Cinek7f657602020-05-21 12:37:14 -0700235 }
236
237 override fun onFinishedWakingUp() {
238 goingToSleep = false
Selim Cinekf630a822020-06-22 11:26:09 -0700239 fullyAwake = true
Selim Cinek7f657602020-05-21 12:37:14 -0700240 }
241
242 override fun onStartedWakingUp() {
243 goingToSleep = false
244 }
Selim Cinekf0f74952020-04-21 11:45:16 -0700245 })
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700246 }
247
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700248 /**
Selim Cinekb52642b2020-04-17 14:30:29 -0700249 * Register a media host and create a view can be attached to a view hierarchy
250 * and where the players will be placed in when the host is the currently desired state.
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700251 *
252 * @return the hostView associated with this location
253 */
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700254 fun register(mediaObject: MediaHost): UniqueObjectHostView {
255 val viewHost = createUniqueObjectHost()
Lucas Dupin989a1112020-05-19 18:56:28 -0700256 mediaObject.hostView = viewHost
Selim Cinek7ba605b2020-06-19 18:34:23 -0700257 mediaObject.addVisibilityChangeListener {
258 // Never animate because of a visibility change, only state changes should do that
259 updateDesiredLocation(forceNoAnimation = true)
260 }
Selim Cinekb52642b2020-04-17 14:30:29 -0700261 mediaHosts[mediaObject.location] = mediaObject
Selim Cinekf0f74952020-04-21 11:45:16 -0700262 if (mediaObject.location == desiredLocation) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700263 // In case we are overriding a view that is already visible, make sure we attach it
264 // to this new host view in the below call
Selim Cinekf0f74952020-04-21 11:45:16 -0700265 desiredLocation = -1
266 }
267 if (mediaObject.location == currentAttachmentLocation) {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700268 currentAttachmentLocation = -1
269 }
Selim Cinekf0f74952020-04-21 11:45:16 -0700270 updateDesiredLocation()
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700271 return viewHost
272 }
273
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700274 private fun createUniqueObjectHost(): UniqueObjectHostView {
Selim Cinek54809622020-04-30 19:04:44 -0700275 val viewHost = UniqueObjectHostView(context)
Selim Cinekf0f74952020-04-21 11:45:16 -0700276 viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
277 override fun onViewAttachedToWindow(p0: View?) {
278 if (rootOverlay == null) {
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700279 rootView = viewHost.viewRootImpl.view
280 rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700281 }
Selim Cinekf0f74952020-04-21 11:45:16 -0700282 viewHost.removeOnAttachStateChangeListener(this)
283 }
284
285 override fun onViewDetachedFromWindow(p0: View?) {
286 }
287 })
288 return viewHost
289 }
290
Selim Cinekf418bb02020-05-04 17:16:58 -0700291 /**
292 * Updates the location that the view should be in. If it changes, an animation may be triggered
293 * going from the old desired location to the new one.
Selim Cinek7ba605b2020-06-19 18:34:23 -0700294 *
295 * @param forceNoAnimation optional parameter telling the system not to animate
Selim Cinekf418bb02020-05-04 17:16:58 -0700296 */
Selim Cinek7ba605b2020-06-19 18:34:23 -0700297 private fun updateDesiredLocation(forceNoAnimation: Boolean = false) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700298 val desiredLocation = calculateLocation()
299 if (desiredLocation != this.desiredLocation) {
300 if (this.desiredLocation >= 0) {
301 previousLocation = this.desiredLocation
302 }
303 val isNewView = this.desiredLocation == -1
304 this.desiredLocation = desiredLocation
305 // Let's perform a transition
Selim Cinek7ba605b2020-06-19 18:34:23 -0700306 val animate = !forceNoAnimation &&
307 shouldAnimateTransition(desiredLocation, previousLocation)
Selim Cinek54809622020-04-30 19:04:44 -0700308 val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700309 val host = getHost(desiredLocation)
Selim Cinekafae4e72020-06-16 18:21:41 -0700310 mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, animate,
311 animDuration, delay)
Selim Cinek54809622020-04-30 19:04:44 -0700312 performTransitionToNewLocation(isNewView, animate)
Selim Cinekf0f74952020-04-21 11:45:16 -0700313 }
314 }
315
Selim Cinek54809622020-04-30 19:04:44 -0700316 private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) {
317 if (previousLocation < 0 || isNewView) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700318 cancelAnimationAndApplyDesiredState()
319 return
320 }
321 val currentHost = getHost(desiredLocation)
322 val previousHost = getHost(previousLocation)
323 if (currentHost == null || previousHost == null) {
324 cancelAnimationAndApplyDesiredState()
Selim Cinek2d7be5f2020-05-01 13:16:01 -0700325 return
Selim Cinekf0f74952020-04-21 11:45:16 -0700326 }
327 updateTargetState()
328 if (isCurrentlyInGuidedTransformation()) {
329 applyTargetStateIfNotAnimating()
Selim Cinek54809622020-04-30 19:04:44 -0700330 } else if (animate) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700331 animator.cancel()
Lucas Dupin989a1112020-05-19 18:56:28 -0700332 if (currentAttachmentLocation == IN_OVERLAY ||
333 !previousHost.hostView.isAttachedToWindow) {
Selim Cinek2d7be5f2020-05-01 13:16:01 -0700334 // Let's animate to the new position, starting from the current position
335 // We also go in here in case the view was detached, since the bounds wouldn't
336 // be correct anymore
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700337 animationStartBounds.set(currentBounds)
Selim Cinek2d7be5f2020-05-01 13:16:01 -0700338 } else {
339 // otherwise, let's take the freshest state, since the current one could
340 // be outdated
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700341 animationStartBounds.set(previousHost.currentBounds)
Selim Cinek2d7be5f2020-05-01 13:16:01 -0700342 }
Selim Cinek54809622020-04-30 19:04:44 -0700343 adjustAnimatorForTransition(desiredLocation, previousLocation)
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700344 rootView?.let {
345 // Let's delay the animation start until we finished laying out
346 animationPending = true
347 it.postOnAnimation(startAnimation)
348 }
Selim Cinekf0f74952020-04-21 11:45:16 -0700349 } else {
350 cancelAnimationAndApplyDesiredState()
351 }
352 }
353
Selim Cinek54809622020-04-30 19:04:44 -0700354 private fun shouldAnimateTransition(
355 @MediaLocation currentLocation: Int,
356 @MediaLocation previousLocation: Int
357 ): Boolean {
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700358 if (isCurrentlyInGuidedTransformation()) {
359 return false
360 }
Lucas Dupin989a1112020-05-19 18:56:28 -0700361 if (currentLocation == LOCATION_QQS &&
362 previousLocation == LOCATION_LOCKSCREEN &&
363 (statusBarStateController.leaveOpenOnKeyguardHide() ||
364 statusbarState == StatusBarState.SHADE_LOCKED)) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700365 // Usually listening to the isShown is enough to determine this, but there is some
366 // non-trivial reattaching logic happening that will make the view not-shown earlier
367 return true
368 }
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700369 return mediaFrame.isShown || animator.isRunning || animationPending
Selim Cinekf0f74952020-04-21 11:45:16 -0700370 }
371
Selim Cinek54809622020-04-30 19:04:44 -0700372 private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
373 val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
374 animator.apply {
Lucas Dupin989a1112020-05-19 18:56:28 -0700375 duration = animDuration
Selim Cinek54809622020-04-30 19:04:44 -0700376 startDelay = delay
377 }
Selim Cinek54809622020-04-30 19:04:44 -0700378 }
379
380 private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
Selim Cinekf0f74952020-04-21 11:45:16 -0700381 var animDuration = 200L
382 var delay = 0L
383 if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
384 // Going to the full shade, let's adjust the animation duration
Lucas Dupin989a1112020-05-19 18:56:28 -0700385 if (statusbarState == StatusBarState.SHADE &&
386 keyguardStateController.isKeyguardFadingAway) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700387 delay = keyguardStateController.keyguardFadingAwayDelay
388 }
389 animDuration = StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE.toLong()
390 } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
391 animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
392 }
Selim Cinek54809622020-04-30 19:04:44 -0700393 return animDuration to delay
Selim Cinekf0f74952020-04-21 11:45:16 -0700394 }
395
396 private fun applyTargetStateIfNotAnimating() {
397 if (!animator.isRunning) {
398 // Let's immediately apply the target state (which is interpolated) if there is
399 // no animation running. Otherwise the animation update will already update
400 // the location
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700401 applyState(targetBounds)
Selim Cinekf0f74952020-04-21 11:45:16 -0700402 }
403 }
404
Selim Cinekf418bb02020-05-04 17:16:58 -0700405 /**
406 * Updates the state that the view wants to be in at the end of the animation.
407 */
Selim Cinekf0f74952020-04-21 11:45:16 -0700408 private fun updateTargetState() {
409 if (isCurrentlyInGuidedTransformation()) {
410 val progress = getTransformationProgress()
411 val currentHost = getHost(desiredLocation)!!
412 val previousHost = getHost(previousLocation)!!
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700413 val newBounds = currentHost.currentBounds
414 val previousBounds = previousHost.currentBounds
415 targetBounds = interpolateBounds(previousBounds, newBounds, progress)
Selim Cinekf0f74952020-04-21 11:45:16 -0700416 } else {
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700417 val bounds = getHost(desiredLocation)?.currentBounds ?: return
418 targetBounds.set(bounds)
Selim Cinekf0f74952020-04-21 11:45:16 -0700419 }
420 }
421
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700422 private fun interpolateBounds(
423 startBounds: Rect,
424 endBounds: Rect,
425 progress: Float,
426 result: Rect? = null
427 ): Rect {
428 val left = MathUtils.lerp(startBounds.left.toFloat(),
429 endBounds.left.toFloat(), progress).toInt()
430 val top = MathUtils.lerp(startBounds.top.toFloat(),
431 endBounds.top.toFloat(), progress).toInt()
432 val right = MathUtils.lerp(startBounds.right.toFloat(),
433 endBounds.right.toFloat(), progress).toInt()
434 val bottom = MathUtils.lerp(startBounds.bottom.toFloat(),
435 endBounds.bottom.toFloat(), progress).toInt()
436 val resultBounds = result ?: Rect()
437 resultBounds.set(left, top, right, bottom)
438 return resultBounds
439 }
440
Selim Cinekf0f74952020-04-21 11:45:16 -0700441 /**
442 * @return true if this transformation is guided by an external progress like a finger
443 */
Lucas Dupin989a1112020-05-19 18:56:28 -0700444 private fun isCurrentlyInGuidedTransformation(): Boolean {
Selim Cinekf0f74952020-04-21 11:45:16 -0700445 return getTransformationProgress() >= 0
446 }
447
448 /**
Lucas Dupin989a1112020-05-19 18:56:28 -0700449 * @return the current transformation progress if we're in a guided transformation and -1
Selim Cinekf0f74952020-04-21 11:45:16 -0700450 * otherwise
451 */
452 private fun getTransformationProgress(): Float {
453 val progress = getQSTransformationProgress()
454 if (progress >= 0) {
455 return progress
456 }
457 return -1.0f
458 }
459
460 private fun getQSTransformationProgress(): Float {
461 val currentHost = getHost(desiredLocation)
462 val previousHost = getHost(previousLocation)
463 if (currentHost?.location == LOCATION_QS) {
464 if (previousHost?.location == LOCATION_QQS) {
465 return qsExpansion
466 }
467 }
468 return -1.0f
469 }
470
471 private fun getHost(@MediaLocation location: Int): MediaHost? {
472 if (location < 0) {
473 return null
474 }
475 return mediaHosts[location]
476 }
477
478 private fun cancelAnimationAndApplyDesiredState() {
479 animator.cancel()
480 getHost(desiredLocation)?.let {
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700481 applyState(it.currentBounds, immediately = true)
Selim Cinekf0f74952020-04-21 11:45:16 -0700482 }
483 }
484
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700485 /**
486 * Apply the current state to the view, updating it's bounds and desired state
487 */
488 private fun applyState(bounds: Rect, immediately: Boolean = false) {
489 currentBounds.set(bounds)
490 val currentlyInGuidedTransformation = isCurrentlyInGuidedTransformation()
491 val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
492 val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
493 val endLocation = desiredLocation
Selim Cinekafae4e72020-06-16 18:21:41 -0700494 mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately)
Selim Cinekf0f74952020-04-21 11:45:16 -0700495 updateHostAttachment()
Selim Cinekf0f74952020-04-21 11:45:16 -0700496 if (currentAttachmentLocation == IN_OVERLAY) {
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700497 mediaFrame.setLeftTopRightBottom(
498 currentBounds.left,
499 currentBounds.top,
500 currentBounds.right,
501 currentBounds.bottom)
Selim Cinekf0f74952020-04-21 11:45:16 -0700502 }
Selim Cinekf0f74952020-04-21 11:45:16 -0700503 }
504
505 private fun updateHostAttachment() {
506 val inOverlay = isTransitionRunning() && rootOverlay != null
507 val newLocation = if (inOverlay) IN_OVERLAY else desiredLocation
508 if (currentAttachmentLocation != newLocation) {
509 currentAttachmentLocation = newLocation
510
511 // Remove the carousel from the old host
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700512 (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
Selim Cinekf0f74952020-04-21 11:45:16 -0700513
514 // Add it to the new one
515 val targetHost = getHost(desiredLocation)!!.hostView
516 if (inOverlay) {
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700517 rootOverlay!!.add(mediaFrame)
Selim Cinekf0f74952020-04-21 11:45:16 -0700518 } else {
Selim Cinek2a1cab12020-06-01 19:19:58 -0700519 // When adding back to the host, let's make sure to reset the bounds.
520 // Usually adding the view will trigger a layout that does this automatically,
521 // but we sometimes suppress this.
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700522 targetHost.addView(mediaFrame)
Selim Cinek2a1cab12020-06-01 19:19:58 -0700523 val left = targetHost.paddingLeft
524 val top = targetHost.paddingTop
525 mediaFrame.setLeftTopRightBottom(
526 left,
527 top,
528 left + currentBounds.width(),
529 top + currentBounds.height())
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700530 }
531 }
532 }
533
Selim Cinekf0f74952020-04-21 11:45:16 -0700534 private fun isTransitionRunning(): Boolean {
Lucas Dupin989a1112020-05-19 18:56:28 -0700535 return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700536 animator.isRunning || animationPending
Selim Cinekb52642b2020-04-17 14:30:29 -0700537 }
538
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700539 @MediaLocation
Lucas Dupin989a1112020-05-19 18:56:28 -0700540 private fun calculateLocation(): Int {
Selim Cinek7f657602020-05-21 12:37:14 -0700541 if (blockLocationChanges) {
542 // Keep the current location until we're allowed to again
543 return desiredLocation
544 }
Lucas Dupin989a1112020-05-19 18:56:28 -0700545 val onLockscreen = (!bypassController.bypassEnabled &&
546 (statusbarState == StatusBarState.KEYGUARD ||
547 statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
548 val allowedOnLockscreen = notifLockscreenUserManager.shouldShowLockscreenNotifications()
Lucas Dupin7d52dd62020-06-18 18:11:22 -0700549 val location = when {
Selim Cinekf0f74952020-04-21 11:45:16 -0700550 qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS
551 qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
Lucas Dupin989a1112020-05-19 18:56:28 -0700552 onLockscreen && allowedOnLockscreen -> LOCATION_LOCKSCREEN
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700553 else -> LOCATION_QQS
554 }
Lucas Dupin7d52dd62020-06-18 18:11:22 -0700555 // When we're on lock screen and the player is not active, we should keep it in QS.
556 // Otherwise it will try to animate a transition that doesn't make sense.
557 if (location == LOCATION_LOCKSCREEN && getHost(location)?.visible != true &&
558 !statusBarStateController.isDozing) {
559 return LOCATION_QS
560 }
Selim Cinekf630a822020-06-22 11:26:09 -0700561 if (location == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS &&
562 collapsingShadeFromQS) {
563 // When collapsing on the lockscreen, we want to remain in QS
564 return LOCATION_QS
565 }
566 if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN
567 && !fullyAwake) {
568 // When unlocking from dozing / while waking up, the media shouldn't be transitioning
569 // in an animated way. Let's keep it in the lockscreen until we're fully awake and
570 // reattach it without an animation
571 return LOCATION_LOCKSCREEN
572 }
Lucas Dupin7d52dd62020-06-18 18:11:22 -0700573 return location
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700574 }
575
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700576 companion object {
577 /**
578 * Attached in expanded quick settings
579 */
580 const val LOCATION_QS = 0
581
582 /**
583 * Attached in the collapsed QS
584 */
585 const val LOCATION_QQS = 1
586
587 /**
588 * Attached on the lock screen
589 */
590 const val LOCATION_LOCKSCREEN = 2
Selim Cinekf0f74952020-04-21 11:45:16 -0700591
592 /**
593 * Attached at the root of the hierarchy in an overlay
594 */
595 const val IN_OVERLAY = -1000
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700596 }
Robert Snoeberger52dfb622020-05-21 16:31:29 -0400597}
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700598
599@IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS,
600 MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN])
601@Retention(AnnotationRetention.SOURCE)
602annotation class MediaLocation