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