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