1 /* <lambda>null2 * 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.controller 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.app.animation.Interpolators 37 import com.android.app.tracing.coroutines.launchTraced as launch 38 import com.android.app.tracing.traceSection 39 import com.android.keyguard.KeyguardViewController 40 import com.android.systemui.Dumpable 41 import com.android.systemui.Flags.mediaControlsLockscreenShadeBugFix 42 import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel 43 import com.android.systemui.dagger.SysUISingleton 44 import com.android.systemui.dagger.qualifiers.Application 45 import com.android.systemui.dagger.qualifiers.Background 46 import com.android.systemui.dreams.DreamOverlayStateController 47 import com.android.systemui.dump.DumpManager 48 import com.android.systemui.keyguard.WakefulnessLifecycle 49 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 50 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager 51 import com.android.systemui.media.controls.ui.view.MediaHost 52 import com.android.systemui.media.dream.MediaDreamComplication 53 import com.android.systemui.plugins.statusbar.StatusBarStateController 54 import com.android.systemui.qs.flags.QSComposeFragment 55 import com.android.systemui.res.R 56 import com.android.systemui.scene.shared.flag.SceneContainerFlag 57 import com.android.systemui.shade.ShadeDisplayAware 58 import com.android.systemui.shade.domain.interactor.ShadeInteractor 59 import com.android.systemui.statusbar.CrossFadeHelper 60 import com.android.systemui.statusbar.StatusBarState 61 import com.android.systemui.statusbar.SysuiStatusBarStateController 62 import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips 63 import com.android.systemui.statusbar.notification.stack.StackStateAnimator 64 import com.android.systemui.statusbar.phone.KeyguardBypassController 65 import com.android.systemui.statusbar.policy.ConfigurationController 66 import com.android.systemui.statusbar.policy.KeyguardStateController 67 import com.android.systemui.statusbar.policy.SplitShadeStateController 68 import com.android.systemui.util.animation.UniqueObjectHostView 69 import com.android.systemui.util.settings.SecureSettings 70 import java.io.PrintWriter 71 import javax.inject.Inject 72 import kotlinx.coroutines.CoroutineScope 73 import kotlinx.coroutines.flow.collectLatest 74 import kotlinx.coroutines.flow.combine 75 import kotlinx.coroutines.flow.distinctUntilChanged 76 import kotlinx.coroutines.flow.mapLatest 77 78 private val TAG: String = MediaHierarchyManager::class.java.simpleName 79 80 /** Similarly to isShown but also excludes views that have 0 alpha */ 81 val View.isShownNotFaded: Boolean 82 get() { 83 var current: View = this 84 while (true) { 85 if (current.visibility != View.VISIBLE) { 86 return false 87 } 88 if (current.alpha == 0.0f) { 89 return false 90 } 91 val parent = current.parent ?: return false // We are not attached to the view root 92 if (parent !is View) { 93 // we reached the viewroot, hurray 94 return true 95 } 96 current = parent 97 } 98 } 99 100 /** 101 * This manager is responsible for placement of the unique media view between the different hosts 102 * and animate the positions of the views to achieve seamless transitions. 103 */ 104 @SysUISingleton 105 class MediaHierarchyManager 106 @Inject 107 constructor( 108 @ShadeDisplayAware private val context: Context, 109 private val statusBarStateController: SysuiStatusBarStateController, 110 private val keyguardStateController: KeyguardStateController, 111 private val bypassController: KeyguardBypassController, 112 private val mediaCarouselController: MediaCarouselController, 113 private val mediaManager: MediaDataManager, 114 private val keyguardViewController: KeyguardViewController, 115 private val dreamOverlayStateController: DreamOverlayStateController, 116 private val keyguardInteractor: KeyguardInteractor, 117 communalTransitionViewModel: CommunalTransitionViewModel, 118 @ShadeDisplayAware configurationController: ConfigurationController, 119 wakefulnessLifecycle: WakefulnessLifecycle, 120 shadeInteractor: ShadeInteractor, 121 private val secureSettings: SecureSettings, 122 @Background private val handler: Handler, 123 @Application private val coroutineScope: CoroutineScope, 124 private val splitShadeStateController: SplitShadeStateController, 125 private val logger: MediaViewLogger, 126 private val dumpManager: DumpManager, 127 ) : Dumpable { 128 129 /** Track the media player setting status on lock screen. */ 130 private var allowMediaPlayerOnLockScreen: Boolean = true 131 private val lockScreenMediaPlayerUri = 132 secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) 133 134 /** 135 * Whether we "skip" QQS during panel expansion. 136 * 137 * This means that when expanding the panel we go directly to QS. Also when we are on QS and 138 * start closing the panel, it fully collapses instead of going to QQS. 139 */ 140 private var skipQqsOnExpansion: Boolean = false 141 142 /** 143 * The root overlay of the hierarchy. This is where the media notification is attached to 144 * whenever the view is transitioning from one host to another. It also make sure that the view 145 * is always in its final state when it is attached to a view host. 146 */ 147 private var rootOverlay: ViewGroupOverlay? = null 148 149 private var rootView: View? = null 150 private var currentBounds = Rect() 151 private var animationStartBounds: Rect = Rect() 152 153 private var animationStartClipping = Rect() 154 private var currentClipping = Rect() 155 private var targetClipping = Rect() 156 157 /** 158 * The cross fade progress at the start of the animation. 0.5f means it's just switching between 159 * the start and the end location and the content is fully faded, while 0.75f means that we're 160 * halfway faded in again in the target state. 161 */ 162 private var animationStartCrossFadeProgress = 0.0f 163 164 /** The starting alpha of the animation */ 165 private var animationStartAlpha = 0.0f 166 167 /** The starting location of the cross fade if an animation is running right now. */ 168 @MediaLocation private var crossFadeAnimationStartLocation = LOCATION_UNKNOWN 169 170 /** The end location of the cross fade if an animation is running right now. */ 171 @MediaLocation private var crossFadeAnimationEndLocation = LOCATION_UNKNOWN 172 private var targetBounds: Rect = Rect() 173 private val mediaFrame 174 get() = mediaCarouselController.mediaFrame 175 176 private var statusbarState: Int = statusBarStateController.state 177 private var animator = <lambda>null178 ValueAnimator.ofFloat(0.0f, 1.0f).apply { 179 interpolator = Interpolators.FAST_OUT_SLOW_IN 180 addUpdateListener { 181 updateTargetState() 182 val currentAlpha: Float 183 var boundsProgress = animatedFraction 184 if (isCrossFadeAnimatorRunning) { 185 animationCrossFadeProgress = 186 MathUtils.lerp(animationStartCrossFadeProgress, 1.0f, animatedFraction) 187 // When crossfading, let's keep the bounds at the right location during fading 188 boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f 189 currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress) 190 } else { 191 // If we're not crossfading, let's interpolate from the start alpha to 1.0f 192 currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction) 193 } 194 interpolateBounds( 195 animationStartBounds, 196 targetBounds, 197 boundsProgress, 198 result = currentBounds, 199 ) 200 resolveClipping(currentClipping) 201 applyState(currentBounds, currentAlpha, clipBounds = currentClipping) 202 } 203 addListener( 204 object : AnimatorListenerAdapter() { 205 private var cancelled: Boolean = false 206 207 override fun onAnimationCancel(animation: Animator) { 208 cancelled = true 209 animationPending = false 210 rootView?.removeCallbacks(startAnimation) 211 isCrossFadeAnimatorRunning = false 212 } 213 214 override fun onAnimationEnd(animation: Animator) { 215 isCrossFadeAnimatorRunning = false 216 if (!cancelled) { 217 applyTargetStateIfNotAnimating() 218 } 219 } 220 221 override fun onAnimationStart(animation: Animator) { 222 cancelled = false 223 animationPending = false 224 } 225 } 226 ) 227 } 228 resolveClippingnull229 private fun resolveClipping(result: Rect) { 230 if (animationStartClipping.isEmpty) result.set(targetClipping) 231 else if (targetClipping.isEmpty) result.set(animationStartClipping) 232 else result.setIntersect(animationStartClipping, targetClipping) 233 } 234 235 private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_STATUS_BAR_POPUP + 1) 236 237 /** 238 * The last location where this view was at before going to the desired location. This is useful 239 * for guided transitions. 240 */ 241 @MediaLocation private var previousLocation = LOCATION_UNKNOWN 242 /** The desired location where the view will be at the end of the transition. */ 243 @MediaLocation private var desiredLocation = LOCATION_UNKNOWN 244 245 /** 246 * The current attachment location where the view is currently attached. Usually this matches 247 * the desired location except for animations whenever a view moves to the new desired location, 248 * during which it is in [IN_OVERLAY]. 249 */ 250 @MediaLocation private var currentAttachmentLocation = LOCATION_UNKNOWN 251 252 private var inSplitShade = false 253 254 /** 255 * Whether we are transitioning to the hub or from the hub to the shade. If so, use fade as the 256 * transformation type and skip calculating state with the bounds and the transition progress. 257 */ 258 private val isHubTransition 259 get() = 260 desiredLocation == LOCATION_COMMUNAL_HUB || 261 (previousLocation == LOCATION_COMMUNAL_HUB && desiredLocation == LOCATION_QS) 262 263 /** Is there any active media or recommendation in the carousel? */ 264 private var hasActiveMediaOrRecommendation: Boolean = false 265 get() = mediaManager.hasActiveMediaOrRecommendation() 266 267 /** Are we currently waiting on an animation to start? */ 268 private var animationPending: Boolean = false <lambda>null269 private val startAnimation: Runnable = Runnable { animator.start() } 270 271 /** The expansion of quick settings */ 272 var qsExpansion: Float = 0.0f 273 set(value) { 274 if (field != value) { 275 field = value 276 updateDesiredLocation() 277 if (getQSTransformationProgress() >= 0) { 278 updateTargetState() 279 applyTargetStateIfNotAnimating() 280 } 281 } 282 } 283 284 /** Is quick setting expanded? */ 285 var qsExpanded: Boolean = false 286 set(value) { 287 if (field != value) { 288 field = value 289 mediaCarouselController.mediaCarouselScrollHandler.qsExpanded = value 290 } 291 updateUserVisibility() 292 } 293 294 /** The expansion fraction of notification shade. */ 295 var shadeExpandedFraction: Float = 0.0f 296 297 /** 298 * distance that the full shade transition takes in order for media to fully transition to the 299 * shade 300 */ 301 private var distanceForFullShadeTransition = 0 302 303 /** 304 * The amount of progress we are currently in if we're transitioning to the full shade. 0.0f 305 * means we're not transitioning yet, while 1 means we're all the way in the full shade. 306 */ 307 private var fullShadeTransitionProgress = 0f 308 set(value) { 309 if (field == value) { 310 return 311 } 312 field = value 313 if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) { 314 // No need to do all the calculations / updates below if we're not on the lockscreen 315 // or if we're bypassing. 316 return 317 } 318 updateDesiredLocation(forceNoAnimation = isCurrentlyFading()) 319 if (value >= 0) { 320 updateTargetState() 321 // Setting the alpha directly, as the below call will use it to update the alpha 322 carouselAlpha = calculateAlphaFromCrossFade(field) 323 applyTargetStateIfNotAnimating() 324 } 325 } 326 327 /** Is there currently a cross-fade animation running driven by an animator? */ 328 private var isCrossFadeAnimatorRunning = false 329 330 /** 331 * Are we currently transitionioning from the lockscreen to the full shade 332 * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and 333 * the transition starts, this will no longer return true. 334 */ 335 private val isTransitioningToFullShade: Boolean 336 get() = 337 fullShadeTransitionProgress != 0f && 338 !bypassController.bypassEnabled && 339 statusbarState == StatusBarState.KEYGUARD 340 341 /** 342 * Set the amount of pixels we have currently dragged down if we're transitioning to the full 343 * shade. 0.0f means we're not transitioning yet. 344 */ setTransitionToFullShadeAmountnull345 fun setTransitionToFullShadeAmount(value: Float) { 346 // If we're transitioning starting on the shade_locked, we don't want any delay and rather 347 // have it aligned with the rest of the animation 348 val progress = MathUtils.saturate(value / distanceForFullShadeTransition) 349 fullShadeTransitionProgress = progress 350 } 351 352 /** 353 * Returns the amount of translationY of the media container, during the current guided 354 * transformation, if running. If there is no guided transformation running, it will return -1. 355 */ getGuidedTransformationTranslationYnull356 fun getGuidedTransformationTranslationY(): Int { 357 if (!isCurrentlyInGuidedTransformation()) { 358 return -1 359 } 360 val startHost = getHost(previousLocation) 361 if (startHost == null || !startHost.visible) { 362 return 0 363 } 364 return targetBounds.top - startHost.currentBounds.top 365 } 366 367 /** 368 * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs, 369 * we wouldn't want to transition in that case. 370 */ 371 var collapsingShadeFromQS: Boolean = false 372 set(value) { 373 if (field != value) { 374 field = value 375 updateDesiredLocation(forceNoAnimation = true) 376 } 377 } 378 379 /** Is the Media Control StatusBarPopup showing */ 380 var isMediaControlPopupShowing: Boolean = false 381 set(value) { 382 if (field != value && StatusBarPopupChips.isEnabled) { 383 field = value 384 updateDesiredLocation(forceNoAnimation = true) 385 } 386 } 387 388 /** Are location changes currently blocked? */ 389 private val blockLocationChanges: Boolean 390 get() { 391 return goingToSleep || dozeAnimationRunning 392 } 393 394 /** Are we currently going to sleep */ 395 private var goingToSleep: Boolean = false 396 set(value) { 397 if (field != value) { 398 field = value 399 if (!value) { 400 updateDesiredLocation() 401 } 402 } 403 } 404 405 /** Are we currently fullyAwake */ 406 private var fullyAwake: Boolean = false 407 set(value) { 408 if (field != value) { 409 field = value 410 if (value) { 411 updateDesiredLocation(forceNoAnimation = true) 412 } 413 } 414 } 415 416 /** Is the doze animation currently Running */ 417 private var dozeAnimationRunning: Boolean = false 418 private set(value) { 419 if (field != value) { 420 field = value 421 if (!value) { 422 updateDesiredLocation() 423 } 424 } 425 } 426 427 /** Is the dream overlay currently active */ 428 private var dreamOverlayActive: Boolean = false 429 private set(value) { 430 if (field != value) { 431 field = value 432 updateDesiredLocation(forceNoAnimation = true) 433 } 434 } 435 436 /** Is the dream media complication currently active */ 437 private var dreamMediaComplicationActive: Boolean = false 438 private set(value) { 439 if (field != value) { 440 field = value 441 updateDesiredLocation(forceNoAnimation = true) 442 } 443 } 444 445 /** Is the communal UI showing */ 446 private var isCommunalShowing: Boolean = false 447 448 /** Is the primary bouncer showing */ 449 private var isPrimaryBouncerShowing: Boolean = false 450 451 /** Is either shade or QS fully expanded */ 452 private var isAnyShadeFullyExpanded: Boolean = false 453 454 /** Is the communal UI showing and not dreaming */ 455 private var onCommunalNotDreaming: Boolean = false 456 457 /** Is the communal UI showing, dreaming and shade expanding */ 458 private var onCommunalDreamingAndShadeExpanding: Boolean = false 459 460 /** 461 * The current cross fade progress. 0.5f means it's just switching between the start and the end 462 * location and the content is fully faded, while 0.75f means that we're halfway faded in again 463 * in the target state. This is only valid while [isCrossFadeAnimatorRunning] is true. 464 */ 465 private var animationCrossFadeProgress = 1.0f 466 467 /** The current carousel Alpha. */ 468 private var carouselAlpha: Float = 1.0f 469 set(value) { 470 if (field == value) { 471 return 472 } 473 field = value 474 CrossFadeHelper.fadeIn(mediaFrame, value) 475 } 476 477 /** 478 * Calculate the alpha of the view when given a cross-fade progress. 479 * 480 * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching 481 * between the start and the end location and the content is fully faded, while 0.75f means 482 * that we're halfway faded in again in the target state. 483 */ calculateAlphaFromCrossFadenull484 private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float { 485 if (crossFadeProgress <= 0.5f) { 486 return 1.0f - crossFadeProgress / 0.5f 487 } else { 488 return (crossFadeProgress - 0.5f) / 0.5f 489 } 490 } 491 492 init { 493 dumpManager.registerNormalDumpable(TAG, this) 494 updateConfiguration() 495 configurationController.addCallback( 496 object : ConfigurationController.ConfigurationListener { onConfigChangednull497 override fun onConfigChanged(newConfig: Configuration?) { 498 updateConfiguration() 499 updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true) 500 } 501 } 502 ) 503 statusBarStateController.addCallback( 504 object : StatusBarStateController.StateListener { onStatePreChangenull505 override fun onStatePreChange(oldState: Int, newState: Int) { 506 // We're updating the location before the state change happens, since we want 507 // the location of the previous state to still be up to date when the animation 508 // starts 509 if ( 510 newState == StatusBarState.SHADE_LOCKED && 511 oldState == StatusBarState.KEYGUARD && 512 fullShadeTransitionProgress < 1.0f 513 ) { 514 // Since the new state is SHADE_LOCKED, we need to set the transition amount 515 // to maximum if the progress is not 1f. 516 setTransitionToFullShadeAmount(distanceForFullShadeTransition.toFloat()) 517 } 518 statusbarState = newState 519 updateDesiredLocation() 520 } 521 onStateChangednull522 override fun onStateChanged(newState: Int) { 523 updateTargetState() 524 updateUserVisibility() 525 } 526 onDozeAmountChangednull527 override fun onDozeAmountChanged(linear: Float, eased: Float) { 528 dozeAnimationRunning = linear != 0.0f && linear != 1.0f 529 } 530 onDozingChangednull531 override fun onDozingChanged(isDozing: Boolean) { 532 if (!isDozing) { 533 dozeAnimationRunning = false 534 } else { 535 updateDesiredLocation() 536 qsExpanded = false 537 closeGuts() 538 } 539 updateUserVisibility() 540 } 541 onExpandedChangednull542 override fun onExpandedChanged(isExpanded: Boolean) { 543 updateUserVisibility() 544 } 545 } 546 ) 547 548 dreamOverlayStateController.addCallback( 549 object : DreamOverlayStateController.Callback { onComplicationsChangednull550 override fun onComplicationsChanged() { 551 dreamMediaComplicationActive = 552 dreamOverlayStateController.complications.any { 553 it is MediaDreamComplication 554 } 555 } 556 onStateChangednull557 override fun onStateChanged() { 558 dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it } 559 } 560 } 561 ) 562 563 wakefulnessLifecycle.addObserver( 564 object : WakefulnessLifecycle.Observer { onFinishedGoingToSleepnull565 override fun onFinishedGoingToSleep() { 566 goingToSleep = false 567 } 568 onStartedGoingToSleepnull569 override fun onStartedGoingToSleep() { 570 goingToSleep = true 571 fullyAwake = false 572 } 573 onFinishedWakingUpnull574 override fun onFinishedWakingUp() { 575 goingToSleep = false 576 fullyAwake = true 577 } 578 onStartedWakingUpnull579 override fun onStartedWakingUp() { 580 goingToSleep = false 581 } 582 } 583 ) 584 585 mediaCarouselController.updateUserVisibility = this::updateUserVisibility <lambda>null586 mediaCarouselController.updateHostVisibility = { 587 mediaHosts.forEach { it?.updateViewVisibility() } 588 } 589 <lambda>null590 coroutineScope.launch { 591 shadeInteractor.isQsBypassingShade.collect { isExpandImmediateEnabled -> 592 skipQqsOnExpansion = isExpandImmediateEnabled 593 updateDesiredLocation() 594 } 595 } 596 <lambda>null597 coroutineScope.launch { 598 shadeInteractor.isAnyFullyExpanded.collect { 599 isAnyShadeFullyExpanded = it 600 updateUserVisibility() 601 } 602 } 603 <lambda>null604 coroutineScope.launch { 605 keyguardInteractor.primaryBouncerShowing.collect { 606 isPrimaryBouncerShowing = it 607 updateUserVisibility() 608 } 609 } 610 611 if (mediaControlsLockscreenShadeBugFix()) { <lambda>null612 coroutineScope.launch { 613 shadeInteractor.shadeExpansion.collect { expansion -> 614 if (expansion >= 1f || expansion <= 0f) { 615 // Shade has fully expanded or collapsed: force transition amount update 616 setTransitionToFullShadeAmount(expansion) 617 } 618 } 619 } 620 } 621 622 val settingsObserver: ContentObserver = 623 object : ContentObserver(handler) { onChangenull624 override fun onChange(selfChange: Boolean, uri: Uri?) { 625 if (uri == lockScreenMediaPlayerUri) { 626 allowMediaPlayerOnLockScreen = 627 secureSettings.getBoolForUser( 628 Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 629 true, 630 UserHandle.USER_CURRENT, 631 ) 632 } 633 } 634 } 635 secureSettings.registerContentObserverForUserAsync( 636 Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 637 settingsObserver, 638 UserHandle.USER_ALL, 639 ) 640 641 // Listen to the communal UI state. Make sure that communal UI is showing and hub itself is 642 // available, ie. not disabled and able to be shown. 643 // When dreaming, qs expansion is immediately set to 1f, so we listen to shade expansion to 644 // calculate the new location. <lambda>null645 coroutineScope.launch { 646 combine( 647 communalTransitionViewModel.isUmoOnCommunal, 648 keyguardInteractor.isDreaming, 649 // keep on communal before the shade is expanded enough to show the elements in 650 // QS 651 shadeInteractor.shadeExpansion 652 .mapLatest { it < EXPANSION_THRESHOLD } 653 .distinctUntilChanged(), 654 ::Triple, 655 ) 656 .collectLatest { (communalShowing, isDreaming, isShadeExpanding) -> 657 isCommunalShowing = communalShowing 658 onCommunalDreamingAndShadeExpanding = 659 communalShowing && isDreaming && isShadeExpanding 660 onCommunalNotDreaming = communalShowing && !isDreaming 661 updateDesiredLocation(forceNoAnimation = true) 662 updateUserVisibility() 663 } 664 } 665 } 666 updateConfigurationnull667 private fun updateConfiguration() { 668 distanceForFullShadeTransition = 669 context.resources.getDimensionPixelSize( 670 R.dimen.lockscreen_shade_media_transition_distance 671 ) 672 inSplitShade = splitShadeStateController.shouldUseSplitNotificationShade(context.resources) 673 } 674 675 /** 676 * Register a media host and create a view can be attached to a view hierarchy and where the 677 * players will be placed in when the host is the currently desired state. 678 * 679 * @return the hostView associated with this location 680 */ registernull681 fun register(mediaObject: MediaHost): UniqueObjectHostView { 682 val viewHost = createUniqueObjectHost() 683 mediaObject.hostView = viewHost 684 mediaObject.addVisibilityChangeListener { 685 // Never animate because of a visibility change, only state changes should do that 686 updateDesiredLocation(forceNoAnimation = true) 687 } 688 mediaHosts[mediaObject.location] = mediaObject 689 if (mediaObject.location == desiredLocation) { 690 // In case we are overriding a view that is already visible, make sure we attach it 691 // to this new host view in the below call 692 desiredLocation = LOCATION_UNKNOWN 693 } 694 if (mediaObject.location == currentAttachmentLocation) { 695 currentAttachmentLocation = LOCATION_UNKNOWN 696 } 697 updateDesiredLocation() 698 return viewHost 699 } 700 701 /** Close the guts in all players in [MediaCarouselController]. */ closeGutsnull702 fun closeGuts() { 703 mediaCarouselController.closeGuts() 704 } 705 createUniqueObjectHostnull706 private fun createUniqueObjectHost(): UniqueObjectHostView { 707 val viewHost = UniqueObjectHostView(context) 708 viewHost.addOnAttachStateChangeListener( 709 object : View.OnAttachStateChangeListener { 710 override fun onViewAttachedToWindow(p0: View) { 711 if (rootOverlay == null) { 712 rootView = viewHost.viewRootImpl.view 713 rootOverlay = (rootView!!.overlay as ViewGroupOverlay) 714 } 715 viewHost.removeOnAttachStateChangeListener(this) 716 } 717 718 override fun onViewDetachedFromWindow(p0: View) {} 719 } 720 ) 721 return viewHost 722 } 723 724 /** 725 * Updates the location that the view should be in. If it changes, an animation may be triggered 726 * going from the old desired location to the new one. 727 * 728 * @param forceNoAnimation optional parameter telling the system not to animate 729 * @param forceStateUpdate optional parameter telling the system to update transition state 730 * 731 * ``` 732 * even if location did not change 733 * ``` 734 */ updateDesiredLocationnull735 private fun updateDesiredLocation( 736 forceNoAnimation: Boolean = false, 737 forceStateUpdate: Boolean = false, 738 ) = 739 traceSection("MediaHierarchyManager#updateDesiredLocation") { 740 val desiredLocation = calculateLocation() 741 if ( 742 desiredLocation != this.desiredLocation || forceStateUpdate && !blockLocationChanges 743 ) { 744 if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) { 745 // Only update previous location when it actually changes 746 previousLocation = this.desiredLocation 747 } else if (forceStateUpdate) { 748 val onLockscreen = 749 (!bypassController.bypassEnabled && 750 (statusbarState == StatusBarState.KEYGUARD)) 751 if ( 752 desiredLocation == LOCATION_QS && 753 previousLocation == LOCATION_LOCKSCREEN && 754 !onLockscreen 755 ) { 756 // If media active state changed and the device is now unlocked, update the 757 // previous location so we animate between the correct hosts 758 previousLocation = LOCATION_QQS 759 } 760 } 761 val isNewView = this.desiredLocation == LOCATION_UNKNOWN 762 this.desiredLocation = desiredLocation 763 // Let's perform a transition 764 val animate = 765 !forceNoAnimation && shouldAnimateTransition(desiredLocation, previousLocation) 766 val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) 767 val host = getHost(desiredLocation) 768 val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE 769 if (!willFade || isCurrentlyInGuidedTransformation() || !animate) { 770 // if we're fading, we want the desired location / measurement only to change 771 // once fully faded. This is happening in the host attachment 772 logger.logMediaLocation("no fade", currentAttachmentLocation, desiredLocation) 773 mediaCarouselController.onDesiredLocationChanged( 774 desiredLocation, 775 host, 776 animate, 777 animDuration, 778 delay, 779 ) 780 } 781 performTransitionToNewLocation(isNewView, animate) 782 } 783 } 784 performTransitionToNewLocationnull785 private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) = 786 traceSection("MediaHierarchyManager#performTransitionToNewLocation") { 787 if (previousLocation < 0 || isNewView) { 788 cancelAnimationAndApplyDesiredState() 789 return 790 } 791 val currentHost = getHost(desiredLocation) 792 val previousHost = getHost(previousLocation) 793 if (currentHost == null || previousHost == null) { 794 cancelAnimationAndApplyDesiredState() 795 return 796 } 797 updateTargetState() 798 if (isCurrentlyInGuidedTransformation()) { 799 applyTargetStateIfNotAnimating() 800 } else if (animate) { 801 val wasCrossFading = isCrossFadeAnimatorRunning 802 val previewsCrossFadeProgress = animationCrossFadeProgress 803 animator.cancel() 804 if ( 805 currentAttachmentLocation != previousLocation || 806 !previousHost.hostView.isAttachedToWindow 807 ) { 808 // Let's animate to the new position, starting from the current position 809 // We also go in here in case the view was detached, since the bounds wouldn't 810 // be correct anymore 811 animationStartBounds.set(currentBounds) 812 animationStartClipping.set(currentClipping) 813 } else { 814 // otherwise, let's take the freshest state, since the current one could 815 // be outdated 816 animationStartBounds.set(previousHost.currentBounds) 817 animationStartClipping.set(previousHost.currentClipping) 818 } 819 val transformationType = calculateTransformationType() 820 var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE 821 var crossFadeStartProgress = 0.0f 822 // The alpha is only relevant when not cross fading 823 var newCrossFadeStartLocation = previousLocation 824 if (wasCrossFading) { 825 if (currentAttachmentLocation == crossFadeAnimationEndLocation) { 826 if (needsCrossFade) { 827 // We were previously crossFading and we've already reached 828 // the end view, Let's start crossfading from the same position there 829 crossFadeStartProgress = 1.0f - previewsCrossFadeProgress 830 } 831 // Otherwise let's fade in from the current alpha, but not cross fade 832 } else { 833 // We haven't reached the previous location yet, let's still cross fade from 834 // where we were. 835 newCrossFadeStartLocation = crossFadeAnimationStartLocation 836 if (newCrossFadeStartLocation == desiredLocation) { 837 // we're crossFading back to where we were, let's start at the end 838 // position 839 crossFadeStartProgress = 1.0f - previewsCrossFadeProgress 840 } else { 841 // Let's start from where we are right now 842 crossFadeStartProgress = previewsCrossFadeProgress 843 // We need to force cross fading as we haven't reached the end location 844 // yet 845 needsCrossFade = true 846 } 847 } 848 } else if (needsCrossFade) { 849 // let's not flicker and start with the same alpha 850 crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f 851 } 852 isCrossFadeAnimatorRunning = needsCrossFade 853 crossFadeAnimationStartLocation = newCrossFadeStartLocation 854 crossFadeAnimationEndLocation = desiredLocation 855 animationStartAlpha = carouselAlpha 856 animationStartCrossFadeProgress = crossFadeStartProgress 857 adjustAnimatorForTransition(desiredLocation, previousLocation) 858 if (!animationPending) { 859 rootView?.let { 860 // Let's delay the animation start until we finished laying out 861 animationPending = true 862 it.postOnAnimation(startAnimation) 863 } 864 } 865 } else { 866 cancelAnimationAndApplyDesiredState() 867 } 868 } 869 shouldAnimateTransitionnull870 private fun shouldAnimateTransition( 871 @MediaLocation currentLocation: Int, 872 @MediaLocation previousLocation: Int, 873 ): Boolean { 874 if (isCurrentlyInGuidedTransformation()) { 875 return false 876 } 877 if ( 878 skipQqsOnExpansion || 879 (QSComposeFragment.isEnabled && 880 desiredLocation == LOCATION_QQS && 881 previousLocation == LOCATION_QS && 882 shadeExpandedFraction == 0.0f) 883 ) { 884 return false 885 } 886 if (isHubTransition) { 887 return false 888 } 889 // This is an invalid transition, and can happen when using the camera gesture from the 890 // lock screen. Disallow. 891 if ( 892 previousLocation == LOCATION_LOCKSCREEN && 893 desiredLocation == LOCATION_QQS && 894 statusbarState == StatusBarState.SHADE 895 ) { 896 return false 897 } 898 899 if ( 900 currentLocation == LOCATION_QQS && 901 previousLocation == LOCATION_LOCKSCREEN && 902 (statusBarStateController.leaveOpenOnKeyguardHide() || 903 statusbarState == StatusBarState.SHADE_LOCKED) 904 ) { 905 // Usually listening to the isShown is enough to determine this, but there is some 906 // non-trivial reattaching logic happening that will make the view not-shown earlier 907 return true 908 } 909 910 if ( 911 desiredLocation == LOCATION_QS && 912 previousLocation == LOCATION_LOCKSCREEN && 913 statusbarState == StatusBarState.SHADE 914 ) { 915 // This is an invalid transition, can happen when tapping on home control and the UMO 916 // while being on landscape orientation in tablet. 917 return false 918 } 919 920 if ( 921 statusbarState == StatusBarState.KEYGUARD && 922 (currentLocation == LOCATION_LOCKSCREEN || previousLocation == LOCATION_LOCKSCREEN) 923 ) { 924 // We're always fading from lockscreen to keyguard in situations where the player 925 // is already fully hidden 926 return false 927 } 928 return mediaFrame.isShownNotFaded || animator.isRunning || animationPending 929 } 930 adjustAnimatorForTransitionnull931 private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) { 932 val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) 933 animator.apply { 934 duration = animDuration 935 startDelay = delay 936 } 937 } 938 getAnimationParamsnull939 private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> { 940 var animDuration = 200L 941 var delay = 0L 942 if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) { 943 // Going to the full shade, let's adjust the animation duration 944 if ( 945 statusbarState == StatusBarState.SHADE && 946 keyguardStateController.isKeyguardFadingAway 947 ) { 948 delay = keyguardStateController.keyguardFadingAwayDelay 949 } 950 animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong() 951 } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) { 952 animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong() 953 } 954 return animDuration to delay 955 } 956 applyTargetStateIfNotAnimatingnull957 private fun applyTargetStateIfNotAnimating() { 958 if (!animator.isRunning) { 959 // Let's immediately apply the target state (which is interpolated) if there is 960 // no animation running. Otherwise the animation update will already update 961 // the location 962 applyState(targetBounds, carouselAlpha, clipBounds = targetClipping) 963 } 964 } 965 966 /** Updates the bounds that the view wants to be in at the end of the animation. */ updateTargetStatenull967 private fun updateTargetState() { 968 var starthost = getHost(previousLocation) 969 var endHost = getHost(desiredLocation) 970 if ( 971 isCurrentlyInGuidedTransformation() && 972 !isCurrentlyFading() && 973 starthost != null && 974 endHost != null 975 ) { 976 val progress = getTransformationProgress() 977 // If either of the hosts are invisible, let's keep them at the other host location to 978 // have a nicer disappear animation. Otherwise the currentBounds of the state might 979 // be undefined 980 if (!endHost.visible) { 981 endHost = starthost 982 } else if (!starthost.visible) { 983 starthost = endHost 984 } 985 val newBounds = endHost.currentBounds 986 val previousBounds = starthost.currentBounds 987 targetBounds = interpolateBounds(previousBounds, newBounds, progress) 988 targetClipping = endHost.currentClipping 989 } else if (endHost != null) { 990 val bounds = endHost.currentBounds 991 targetBounds.set(bounds) 992 targetClipping = endHost.currentClipping 993 } 994 } 995 interpolateBoundsnull996 private fun interpolateBounds( 997 startBounds: Rect, 998 endBounds: Rect, 999 progress: Float, 1000 result: Rect? = null, 1001 ): Rect { 1002 val left = 1003 MathUtils.lerp(startBounds.left.toFloat(), endBounds.left.toFloat(), progress).toInt() 1004 val top = 1005 MathUtils.lerp(startBounds.top.toFloat(), endBounds.top.toFloat(), progress).toInt() 1006 val right = 1007 MathUtils.lerp(startBounds.right.toFloat(), endBounds.right.toFloat(), progress).toInt() 1008 val bottom = 1009 MathUtils.lerp(startBounds.bottom.toFloat(), endBounds.bottom.toFloat(), progress) 1010 .toInt() 1011 val resultBounds = result ?: Rect() 1012 resultBounds.set(left, top, right, bottom) 1013 return resultBounds 1014 } 1015 1016 /** @return true if this transformation is guided by an external progress like a finger */ isCurrentlyInGuidedTransformationnull1017 fun isCurrentlyInGuidedTransformation(): Boolean { 1018 return hasValidStartAndEndLocations() && 1019 getTransformationProgress() >= 0 && 1020 (areGuidedTransitionHostsVisible() || !hasActiveMediaOrRecommendation) 1021 } 1022 hasValidStartAndEndLocationsnull1023 private fun hasValidStartAndEndLocations(): Boolean { 1024 return previousLocation != LOCATION_UNKNOWN && desiredLocation != LOCATION_UNKNOWN 1025 } 1026 1027 /** Calculate the transformation type for the current animation */ 1028 @VisibleForTesting 1029 @TransformationType calculateTransformationTypenull1030 fun calculateTransformationType(): Int { 1031 if (isHubTransition) { 1032 return TRANSFORMATION_TYPE_FADE 1033 } 1034 if (isTransitioningToFullShade) { 1035 if (inSplitShade && areGuidedTransitionHostsVisible()) { 1036 return TRANSFORMATION_TYPE_TRANSITION 1037 } 1038 return TRANSFORMATION_TYPE_FADE 1039 } 1040 if ( 1041 previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS || 1042 previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN 1043 ) { 1044 // animating between ls and qs should fade, as QS is clipped. 1045 return TRANSFORMATION_TYPE_FADE 1046 } 1047 if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) { 1048 // animating between ls and qqs should fade when dragging down via e.g. expand button 1049 return TRANSFORMATION_TYPE_FADE 1050 } 1051 return TRANSFORMATION_TYPE_TRANSITION 1052 } 1053 areGuidedTransitionHostsVisiblenull1054 private fun areGuidedTransitionHostsVisible(): Boolean { 1055 return getHost(previousLocation)?.visible == true && 1056 getHost(desiredLocation)?.visible == true 1057 } 1058 1059 /** 1060 * @return the current transformation progress if we're in a guided transformation and -1 1061 * otherwise 1062 */ getTransformationProgressnull1063 private fun getTransformationProgress(): Float { 1064 if (skipQqsOnExpansion || isHubTransition) { 1065 return -1.0f 1066 } 1067 val progress = getQSTransformationProgress() 1068 if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) { 1069 return progress 1070 } 1071 if (isTransitioningToFullShade) { 1072 return fullShadeTransitionProgress 1073 } 1074 return -1.0f 1075 } 1076 getQSTransformationProgressnull1077 private fun getQSTransformationProgress(): Float { 1078 val currentHost = getHost(desiredLocation) 1079 val previousHost = getHost(previousLocation) 1080 if (currentHost?.location == LOCATION_QS && !inSplitShade) { 1081 if (previousHost?.location == LOCATION_QQS) { 1082 if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) { 1083 return qsExpansion 1084 } 1085 } 1086 } 1087 return -1.0f 1088 } 1089 getHostnull1090 private fun getHost(@MediaLocation location: Int): MediaHost? { 1091 if (location < 0) { 1092 return null 1093 } 1094 return mediaHosts[location] 1095 } 1096 cancelAnimationAndApplyDesiredStatenull1097 private fun cancelAnimationAndApplyDesiredState() { 1098 animator.cancel() 1099 getHost(desiredLocation)?.let { 1100 applyState(it.currentBounds, alpha = 1.0f, immediately = true) 1101 } 1102 } 1103 1104 /** Apply the current state to the view, updating it's bounds and desired state */ applyStatenull1105 private fun applyState( 1106 bounds: Rect, 1107 alpha: Float, 1108 immediately: Boolean = false, 1109 clipBounds: Rect = EMPTY_RECT, 1110 ) = 1111 traceSection("MediaHierarchyManager#applyState") { 1112 currentBounds.set(bounds) 1113 currentClipping = clipBounds 1114 carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f 1115 val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading() 1116 val startLocation = if (onlyUseEndState) LOCATION_UNKNOWN else previousLocation 1117 val progress = if (onlyUseEndState) 1.0f else getTransformationProgress() 1118 val endLocation = resolveLocationForFading() 1119 mediaCarouselController.setCurrentState( 1120 startLocation, 1121 endLocation, 1122 progress, 1123 immediately, 1124 ) 1125 updateHostAttachment() 1126 if (currentAttachmentLocation == IN_OVERLAY) { 1127 // Setting the clipping on the hierarchy of `mediaFrame` does not work 1128 if (!currentClipping.isEmpty) { 1129 currentBounds.intersect(currentClipping) 1130 } 1131 mediaFrame.setLeftTopRightBottom( 1132 currentBounds.left, 1133 currentBounds.top, 1134 currentBounds.right, 1135 currentBounds.bottom, 1136 ) 1137 } 1138 } 1139 updateHostAttachmentnull1140 private fun updateHostAttachment() = 1141 traceSection("MediaHierarchyManager#updateHostAttachment") { 1142 if (SceneContainerFlag.isEnabled) { 1143 // No need to manage transition states - just update the desired location directly 1144 val host = getHost(desiredLocation) 1145 logger.logMediaHostAttachment(desiredLocation, host?.visible) 1146 mediaCarouselController.onDesiredLocationChanged( 1147 desiredLocation = desiredLocation, 1148 desiredHostState = host, 1149 animate = false, 1150 ) 1151 return 1152 } 1153 1154 var newLocation = resolveLocationForFading() 1155 // Don't use the overlay when fading or when we don't have active media 1156 var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation 1157 if (isCrossFadeAnimatorRunning) { 1158 if ( 1159 getHost(newLocation)?.visible == true && 1160 getHost(newLocation)?.hostView?.isShown == false && 1161 newLocation != desiredLocation 1162 ) { 1163 // We're crossfading but the view is already hidden. Let's move to the overlay 1164 // instead. This happens when animating to the full shade using a button click. 1165 canUseOverlay = true 1166 } 1167 } 1168 val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay 1169 newLocation = if (inOverlay) IN_OVERLAY else newLocation 1170 if (currentAttachmentLocation != newLocation) { 1171 currentAttachmentLocation = newLocation 1172 1173 // Remove the carousel from the old host 1174 (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame) 1175 1176 // Add it to the new one 1177 if (inOverlay) { 1178 rootOverlay!!.add(mediaFrame) 1179 } else { 1180 val targetHost = getHost(newLocation)!!.hostView 1181 // This will either do a full layout pass and remeasure, or it will bypass 1182 // that and directly set the mediaFrame's bounds within the premeasured host. 1183 targetHost.addView(mediaFrame) 1184 } 1185 val host = getHost(currentAttachmentLocation) 1186 logger.logMediaHostAttachment(currentAttachmentLocation, host?.visible) 1187 if (isCrossFadeAnimatorRunning) { 1188 // When cross-fading with an animation, we only notify the media carousel of the 1189 // location change, once the view is reattached to the new place and not 1190 // immediately 1191 // when the desired location changes. This callback will update the measurement 1192 // of the carousel, only once we've faded out at the old location and then 1193 // reattach to fade it in at the new location. 1194 logger.logMediaLocation("crossfade", currentAttachmentLocation, newLocation) 1195 mediaCarouselController.onDesiredLocationChanged( 1196 newLocation, 1197 getHost(newLocation), 1198 animate = false, 1199 ) 1200 } 1201 } 1202 } 1203 1204 /** 1205 * Calculate the location when cross fading between locations. While fading out, the content 1206 * should remain in the previous location, while after the switch it should be at the desired 1207 * location. 1208 */ 1209 @MediaLocation resolveLocationForFadingnull1210 private fun resolveLocationForFading(): Int { 1211 if (isCrossFadeAnimatorRunning) { 1212 // When animating between two hosts with a fade, let's keep ourselves in the old 1213 // location for the first half, and then switch over to the end location 1214 if (animationCrossFadeProgress > 0.5 || previousLocation == LOCATION_UNKNOWN) { 1215 return crossFadeAnimationEndLocation 1216 } else { 1217 return crossFadeAnimationStartLocation 1218 } 1219 } 1220 return desiredLocation 1221 } 1222 isTransitionRunningnull1223 private fun isTransitionRunning(): Boolean { 1224 return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f || 1225 animator.isRunning || 1226 animationPending 1227 } 1228 1229 @MediaLocation calculateLocationnull1230 private fun calculateLocation(): Int { 1231 if (blockLocationChanges) { 1232 // Keep the current location until we're allowed to again 1233 return desiredLocation 1234 } 1235 1236 val onLockscreen = 1237 (!bypassController.bypassEnabled && (statusbarState == StatusBarState.KEYGUARD)) 1238 1239 // UMO should show on hub unless the qs is expanding when not dreaming, or shade is 1240 // expanding when dreaming 1241 val onCommunal = 1242 (onCommunalNotDreaming && qsExpansion == 0.0f) || onCommunalDreamingAndShadeExpanding 1243 val location = 1244 when { 1245 isMediaControlPopupShowing && StatusBarPopupChips.isEnabled -> 1246 LOCATION_STATUS_BAR_POPUP 1247 dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY 1248 onCommunal -> LOCATION_COMMUNAL_HUB 1249 (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS 1250 qsExpansion > EXPANSION_THRESHOLD && onLockscreen -> LOCATION_QS 1251 onLockscreen && isSplitShadeExpanding() -> LOCATION_QS 1252 onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS 1253 1254 // Communal does not have its own StatusBarState so it should always have higher 1255 // priority for the UMO over the lockscreen. 1256 isCommunalShowing -> LOCATION_COMMUNAL_HUB 1257 onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN 1258 else -> LOCATION_QQS 1259 } 1260 // When we're on lock screen and the player is not active, we should keep it in QS. 1261 // Otherwise it will try to animate a transition that doesn't make sense. 1262 if ( 1263 location == LOCATION_LOCKSCREEN && 1264 getHost(location)?.visible != true && 1265 !statusBarStateController.isDozing 1266 ) { 1267 return LOCATION_QS 1268 } 1269 if ( 1270 location == LOCATION_LOCKSCREEN && 1271 desiredLocation == LOCATION_QS && 1272 collapsingShadeFromQS 1273 ) { 1274 // When collapsing on the lockscreen, we want to remain in QS 1275 return LOCATION_QS 1276 } 1277 if ( 1278 location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && !fullyAwake 1279 ) { 1280 // When unlocking from dozing / while waking up, the media shouldn't be transitioning 1281 // in an animated way. Let's keep it in the lockscreen until we're fully awake and 1282 // reattach it without an animation 1283 return LOCATION_LOCKSCREEN 1284 } 1285 // When communal showing while dreaming, skipQqsOnExpansion is also true but we want to 1286 // return the calculated location, so it won't disappear as soon as shade is pulled down. 1287 if (isCommunalShowing) return location 1288 if (skipQqsOnExpansion) { 1289 // When doing an immediate expand or collapse, we want to keep it in QS. 1290 return LOCATION_QS 1291 } 1292 return location 1293 } 1294 isSplitShadeExpandingnull1295 private fun isSplitShadeExpanding(): Boolean { 1296 return inSplitShade && isTransitioningToFullShade 1297 } 1298 1299 /** Are we currently transforming to the full shade and already in QQS */ isTransformingToFullShadeAndInQQSnull1300 private fun isTransformingToFullShadeAndInQQS(): Boolean { 1301 if (!isTransitioningToFullShade) { 1302 return false 1303 } 1304 if (inSplitShade) { 1305 // Split shade doesn't use QQS. 1306 return false 1307 } 1308 return fullShadeTransitionProgress > 0.5f 1309 } 1310 1311 /** Is the current transformationType fading */ isCurrentlyFadingnull1312 private fun isCurrentlyFading(): Boolean { 1313 if (isSplitShadeExpanding()) { 1314 // Split shade always uses transition instead of fade. 1315 return false 1316 } 1317 if (isTransitioningToFullShade) { 1318 return true 1319 } 1320 return isCrossFadeAnimatorRunning 1321 } 1322 1323 /** Update whether or not the media carousel could be visible to the user */ updateUserVisibilitynull1324 private fun updateUserVisibility() { 1325 val shadeVisible = 1326 isLockScreenVisibleToUser() || 1327 isLockScreenShadeVisibleToUser() || 1328 isHomeScreenShadeVisibleToUser() || 1329 isGlanceableHubVisibleToUser() 1330 val mediaVisible = qsExpanded || hasActiveMediaOrRecommendation 1331 logger.logUserVisibilityChange(shadeVisible, mediaVisible) 1332 mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = 1333 shadeVisible && mediaVisible 1334 } 1335 isLockScreenVisibleToUsernull1336 private fun isLockScreenVisibleToUser(): Boolean { 1337 return !statusBarStateController.isDozing && 1338 !keyguardViewController.isBouncerShowing && 1339 statusBarStateController.state == StatusBarState.KEYGUARD && 1340 allowMediaPlayerOnLockScreen && 1341 statusBarStateController.isExpanded && 1342 !qsExpanded 1343 } 1344 isLockScreenShadeVisibleToUsernull1345 private fun isLockScreenShadeVisibleToUser(): Boolean { 1346 return !statusBarStateController.isDozing && 1347 !keyguardViewController.isBouncerShowing && 1348 (statusBarStateController.state == StatusBarState.SHADE_LOCKED || 1349 (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded)) 1350 } 1351 isHomeScreenShadeVisibleToUsernull1352 private fun isHomeScreenShadeVisibleToUser(): Boolean { 1353 return !statusBarStateController.isDozing && 1354 statusBarStateController.state == StatusBarState.SHADE && 1355 statusBarStateController.isExpanded 1356 } 1357 isGlanceableHubVisibleToUsernull1358 private fun isGlanceableHubVisibleToUser(): Boolean { 1359 return isCommunalShowing && !isPrimaryBouncerShowing && !isAnyShadeFullyExpanded 1360 } 1361 dumpnull1362 override fun dump(pw: PrintWriter, args: Array<out String>) { 1363 pw.apply { 1364 println( 1365 "current attachment: $currentAttachmentLocation, " + 1366 "desired location: $desiredLocation, " + 1367 "visible ${getHost(desiredLocation)?.visible}" 1368 ) 1369 println("previous location: $previousLocation") 1370 println("bounds: $currentBounds, target $targetBounds") 1371 println("clipping: $currentClipping, target $targetClipping") 1372 } 1373 } 1374 1375 companion object { 1376 /** Attached in expanded quick settings */ 1377 const val LOCATION_QS = 0 1378 1379 /** Attached in the collapsed QS */ 1380 const val LOCATION_QQS = 1 1381 1382 /** Attached on the lock screen */ 1383 const val LOCATION_LOCKSCREEN = 2 1384 1385 /** Attached on the dream overlay */ 1386 const val LOCATION_DREAM_OVERLAY = 3 1387 1388 /** Attached to a view in the communal UI grid */ 1389 const val LOCATION_COMMUNAL_HUB = 4 1390 1391 /** Attached to a popup that is shown with a media control chip in the status bar */ 1392 const val LOCATION_STATUS_BAR_POPUP = 5 1393 1394 /** Attached at the root of the hierarchy in an overlay */ 1395 const val IN_OVERLAY = -1000 1396 1397 /** Not attached to any view */ 1398 const val LOCATION_UNKNOWN = -1 1399 1400 /** 1401 * The default transformation type where the hosts transform into each other using a direct 1402 * transition 1403 */ 1404 const val TRANSFORMATION_TYPE_TRANSITION = 0 1405 1406 /** 1407 * A transformation type where content fades from one place to another instead of 1408 * transitioning 1409 */ 1410 const val TRANSFORMATION_TYPE_FADE = 1 1411 1412 /** Expansion amount value at which elements start to become visible in the QS panel. */ 1413 const val EXPANSION_THRESHOLD = 0.4f 1414 } 1415 } 1416 1417 private val EMPTY_RECT = Rect() 1418 1419 @IntDef( 1420 prefix = ["TRANSFORMATION_TYPE_"], 1421 value = 1422 [ 1423 MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION, 1424 MediaHierarchyManager.TRANSFORMATION_TYPE_FADE, 1425 ], 1426 ) 1427 @Retention(AnnotationRetention.SOURCE) 1428 private annotation class TransformationType 1429 1430 @IntDef( 1431 prefix = ["LOCATION_"], 1432 value = 1433 [ 1434 MediaHierarchyManager.LOCATION_QS, 1435 MediaHierarchyManager.LOCATION_QQS, 1436 MediaHierarchyManager.LOCATION_LOCKSCREEN, 1437 MediaHierarchyManager.LOCATION_DREAM_OVERLAY, 1438 MediaHierarchyManager.LOCATION_COMMUNAL_HUB, 1439 MediaHierarchyManager.LOCATION_STATUS_BAR_POPUP, 1440 MediaHierarchyManager.LOCATION_UNKNOWN, 1441 ], 1442 ) 1443 @Retention(AnnotationRetention.SOURCE) 1444 annotation class MediaLocation 1445