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