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