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