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.AnimatorInflater 21 import android.animation.AnimatorSet 22 import android.content.Context 23 import android.content.res.Configuration 24 import android.graphics.Color 25 import android.graphics.Paint 26 import android.graphics.Typeface 27 import android.graphics.drawable.Drawable 28 import android.provider.Settings 29 import android.view.View 30 import android.view.animation.Interpolator 31 import androidx.annotation.VisibleForTesting 32 import androidx.constraintlayout.widget.ConstraintSet 33 import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT 34 import com.android.app.animation.Interpolators 35 import com.android.app.tracing.traceSection 36 import com.android.systemui.Flags 37 import com.android.systemui.dagger.qualifiers.Main 38 import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition 39 import com.android.systemui.media.controls.ui.animation.MetadataAnimationHandler 40 import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder 41 import com.android.systemui.media.controls.ui.binder.SeekBarObserver 42 import com.android.systemui.media.controls.ui.controller.MediaCarouselController.Companion.calculateAlpha 43 import com.android.systemui.media.controls.ui.view.GutsViewHolder 44 import com.android.systemui.media.controls.ui.view.MediaHostState 45 import com.android.systemui.media.controls.ui.view.MediaViewHolder 46 import com.android.systemui.media.controls.ui.view.MediaViewHolder.Companion.headlineSmallTF 47 import com.android.systemui.media.controls.ui.view.MediaViewHolder.Companion.labelLargeTF 48 import com.android.systemui.media.controls.ui.view.MediaViewHolder.Companion.labelMediumTF 49 import com.android.systemui.media.controls.ui.view.MediaViewHolder.Companion.titleMediumTF 50 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel 51 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel 52 import com.android.systemui.res.R 53 import com.android.systemui.scene.shared.flag.SceneContainerFlag 54 import com.android.systemui.statusbar.policy.ConfigurationController 55 import com.android.systemui.surfaceeffects.PaintDrawCallback 56 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect 57 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView 58 import com.android.systemui.surfaceeffects.ripple.MultiRippleController 59 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig 60 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController 61 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader 62 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView 63 import com.android.systemui.util.animation.MeasurementInput 64 import com.android.systemui.util.animation.MeasurementOutput 65 import com.android.systemui.util.animation.TransitionLayout 66 import com.android.systemui.util.animation.TransitionLayoutController 67 import com.android.systemui.util.animation.TransitionViewState 68 import com.android.systemui.util.concurrency.DelayableExecutor 69 import com.android.systemui.util.settings.GlobalSettings 70 import java.lang.Float.max 71 import java.lang.Float.min 72 import java.util.Random 73 import javax.inject.Inject 74 75 /** 76 * A class responsible for controlling a single instance of a media player handling interactions 77 * with the view instance and keeping the media view states up to date. 78 */ 79 open class MediaViewController 80 @Inject 81 constructor( 82 @Main private val context: Context, 83 @Main private val configurationController: ConfigurationController, 84 private val mediaHostStatesManager: MediaHostStatesManager, 85 private val logger: MediaViewLogger, 86 private val seekBarViewModel: SeekBarViewModel, 87 @Main private val mainExecutor: DelayableExecutor, 88 private val globalSettings: GlobalSettings, 89 ) { 90 91 companion object { 92 @JvmField val GUTS_ANIMATION_DURATION = 234L 93 } 94 95 /** A listener when the current dimensions of the player change */ 96 lateinit var sizeChangedListener: () -> Unit 97 lateinit var configurationChangeListener: () -> Unit 98 lateinit var recsConfigurationChangeListener: (MediaViewController, TransitionLayout) -> Unit 99 var locationChangeListener: (Int) -> Unit = {} 100 private var firstRefresh: Boolean = true 101 @VisibleForTesting private var transitionLayout: TransitionLayout? = null 102 private val layoutController = TransitionLayoutController() 103 private var animationDelay: Long = 0 104 private var animationDuration: Long = 0 105 private var animateNextStateChange: Boolean = false 106 private val measurement = MeasurementOutput(0, 0) 107 108 /** A map containing all viewStates for all locations of this mediaState */ 109 private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf() 110 111 /** 112 * The ending location of the view where it ends when all animations and transitions have 113 * finished 114 */ 115 @MediaLocation 116 var currentEndLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN 117 set(value) { 118 if (field != value) { 119 field = value 120 if (!SceneContainerFlag.isEnabled) return 121 locationChangeListener(value) 122 } 123 } 124 125 /** The starting location of the view where it starts for all animations and transitions */ 126 @MediaLocation private var currentStartLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN 127 128 /** The progress of the transition or 1.0 if there is no transition happening */ 129 private var currentTransitionProgress: Float = 1.0f 130 131 /** A temporary state used to store intermediate measurements. */ 132 private val tmpState = TransitionViewState() 133 134 /** A temporary state used to store intermediate measurements. */ 135 private val tmpState2 = TransitionViewState() 136 137 /** A temporary state used to store intermediate measurements. */ 138 private val tmpState3 = TransitionViewState() 139 140 /** A temporary cache key to be used to look up cache entries */ 141 private val tmpKey = CacheKey() 142 143 /** 144 * The current width of the player. This might not factor in case the player is animating to the 145 * current state, but represents the end state 146 */ 147 var currentWidth: Int = 0 148 /** 149 * The current height of the player. This might not factor in case the player is animating to 150 * the current state, but represents the end state 151 */ 152 var currentHeight: Int = 0 153 154 /** Get the translationX of the layout */ 155 var translationX: Float = 0.0f 156 private set 157 get() { 158 return transitionLayout?.translationX ?: 0.0f 159 } 160 161 /** Get the translationY of the layout */ 162 var translationY: Float = 0.0f 163 private set 164 get() { 165 return transitionLayout?.translationY ?: 0.0f 166 } 167 168 /** Whether artwork is bound. */ 169 var isArtworkBound: Boolean = false 170 171 /** previous background artwork */ 172 var prevArtwork: Drawable? = null 173 174 /** Whether scrubbing time can show */ 175 var canShowScrubbingTime: Boolean = false 176 177 /** Whether user is touching the seek bar to change the position */ 178 var isScrubbing: Boolean = false 179 180 var isSeekBarEnabled: Boolean = false 181 182 /** Whether font family should be updated. */ 183 private var isFontUpdateAllowed: Boolean = true 184 185 /** Not visible value for previous button when scrubbing */ 186 private var prevNotVisibleValue = ConstraintSet.GONE 187 private var isPrevButtonAvailable = false 188 189 /** Not visible value for next button when scrubbing */ 190 private var nextNotVisibleValue = ConstraintSet.GONE 191 private var isNextButtonAvailable = false 192 193 /** View holders for controller */ 194 var mediaViewHolder: MediaViewHolder? = null 195 196 private lateinit var seekBarObserver: SeekBarObserver 197 private lateinit var turbulenceNoiseController: TurbulenceNoiseController 198 private lateinit var loadingEffect: LoadingEffect 199 private lateinit var turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig 200 private lateinit var noiseDrawCallback: PaintDrawCallback 201 private lateinit var stateChangedCallback: LoadingEffect.AnimationStateChangedCallback 202 internal lateinit var metadataAnimationHandler: MetadataAnimationHandler 203 internal lateinit var colorSchemeTransition: ColorSchemeTransition 204 internal lateinit var multiRippleController: MultiRippleController 205 206 private val scrubbingChangeListener = 207 object : SeekBarViewModel.ScrubbingChangeListener { 208 override fun onScrubbingChanged(scrubbing: Boolean) { 209 if (!SceneContainerFlag.isEnabled) return 210 if (isScrubbing == scrubbing) return 211 isScrubbing = scrubbing 212 updateDisplayForScrubbingChange() 213 } 214 } 215 216 private val enabledChangeListener = 217 object : SeekBarViewModel.EnabledChangeListener { 218 override fun onEnabledChanged(enabled: Boolean) { 219 if (!SceneContainerFlag.isEnabled) return 220 if (isSeekBarEnabled == enabled) return 221 isSeekBarEnabled = enabled 222 MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled) 223 mainExecutor.execute { 224 if (!metadataAnimationHandler.isRunning) { 225 // Trigger a state refresh so that we immediately update visibilities. 226 refreshState() 227 } 228 } 229 } 230 } 231 232 private val seekbarDescriptionListener = 233 object : SeekBarViewModel.ContentDescriptionListener { 234 override fun onContentDescriptionChanged( 235 elapsedTimeDescription: CharSequence, 236 durationDescription: CharSequence, 237 ) { 238 if (!SceneContainerFlag.isEnabled) return 239 mainExecutor.execute { 240 seekBarObserver.updateContentDescription( 241 elapsedTimeDescription, 242 durationDescription, 243 ) 244 } 245 } 246 } 247 248 /** 249 * Sets the listening state of the player. 250 * 251 * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid 252 * unnecessary work when the QS panel is closed. 253 * 254 * @param listening True when player should be active. Otherwise, false. 255 */ 256 fun setListening(listening: Boolean) { 257 if (!SceneContainerFlag.isEnabled) return 258 seekBarViewModel.listening = listening 259 } 260 261 /** A callback for config changes */ 262 private val configurationListener = 263 object : ConfigurationController.ConfigurationListener { 264 var lastOrientation = -1 265 266 override fun onConfigChanged(newConfig: Configuration?) { 267 // Because the TransitionLayout is not always attached (and calculates/caches layout 268 // results regardless of attach state), we have to force the layoutDirection of the 269 // view 270 // to the correct value for the user's current locale to ensure correct 271 // recalculation 272 // when/after calling refreshState() 273 newConfig?.apply { 274 if (transitionLayout?.rawLayoutDirection != layoutDirection) { 275 transitionLayout?.layoutDirection = layoutDirection 276 refreshState() 277 } 278 val newOrientation = newConfig.orientation 279 if (lastOrientation != newOrientation) { 280 // Layout dimensions are possibly changing, so we need to update them. (at 281 // least on large screen devices) 282 lastOrientation = newOrientation 283 // Update the height of media controls for the expanded layout. it is needed 284 // for large screen devices. 285 setBackgroundHeights( 286 context.resources.getDimensionPixelSize( 287 R.dimen.qs_media_session_height_expanded 288 ) 289 ) 290 } 291 if (SceneContainerFlag.isEnabled) { 292 if ( 293 this@MediaViewController::recsConfigurationChangeListener.isInitialized 294 ) { 295 transitionLayout?.let { 296 recsConfigurationChangeListener.invoke(this@MediaViewController, it) 297 } 298 } 299 } else if ( 300 this@MediaViewController::configurationChangeListener.isInitialized 301 ) { 302 configurationChangeListener.invoke() 303 refreshState() 304 } 305 } 306 } 307 } 308 309 /** A callback for media state changes */ 310 val stateCallback = 311 object : MediaHostStatesManager.Callback { 312 override fun onHostStateChanged( 313 @MediaLocation location: Int, 314 mediaHostState: MediaHostState, 315 ) { 316 if (location == currentEndLocation || location == currentStartLocation) { 317 setCurrentState( 318 currentStartLocation, 319 currentEndLocation, 320 currentTransitionProgress, 321 applyImmediately = false, 322 ) 323 } 324 } 325 } 326 327 /** 328 * The expanded constraint set used to render a expanded player. If it is modified, make sure to 329 * call [refreshState] 330 */ 331 var collapsedLayout = ConstraintSet() 332 @VisibleForTesting set 333 334 /** 335 * The expanded constraint set used to render a collapsed player. If it is modified, make sure 336 * to call [refreshState] 337 */ 338 var expandedLayout = ConstraintSet() 339 @VisibleForTesting set 340 341 /** Whether the guts are visible for the associated player. */ 342 var isGutsVisible = false 343 private set 344 345 /** Size provided by the scene framework container */ 346 var widthInSceneContainerPx = 0 347 var heightInSceneContainerPx = 0 348 349 init { 350 mediaHostStatesManager.addController(this) 351 layoutController.sizeChangedListener = { width: Int, height: Int -> 352 currentWidth = width 353 currentHeight = height 354 sizeChangedListener.invoke() 355 } 356 configurationController.addCallback(configurationListener) 357 } 358 359 /** 360 * Notify this controller that the view has been removed and all listeners should be destroyed 361 */ 362 fun onDestroy() { 363 if (SceneContainerFlag.isEnabled) { 364 if (this::seekBarObserver.isInitialized) { 365 seekBarViewModel.progress.removeObserver(seekBarObserver) 366 } 367 seekBarViewModel.removeScrubbingChangeListener(scrubbingChangeListener) 368 seekBarViewModel.removeEnabledChangeListener(enabledChangeListener) 369 seekBarViewModel.removeContentDescriptionListener(seekbarDescriptionListener) 370 seekBarViewModel.onDestroy() 371 } 372 mediaHostStatesManager.removeController(this) 373 configurationController.removeCallback(configurationListener) 374 } 375 376 /** Show guts with an animated transition. */ 377 fun openGuts() { 378 if (isGutsVisible) return 379 isGutsVisible = true 380 animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) 381 setCurrentState( 382 currentStartLocation, 383 currentEndLocation, 384 currentTransitionProgress, 385 applyImmediately = false, 386 isGutsAnimation = true, 387 ) 388 } 389 390 /** 391 * Close the guts for the associated player. 392 * 393 * @param immediate if `false`, it will animate the transition. 394 */ 395 @JvmOverloads 396 fun closeGuts(immediate: Boolean = false) { 397 if (!isGutsVisible) return 398 isGutsVisible = false 399 if (!immediate) { 400 animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) 401 } 402 setCurrentState( 403 currentStartLocation, 404 currentEndLocation, 405 currentTransitionProgress, 406 applyImmediately = immediate, 407 isGutsAnimation = true, 408 ) 409 } 410 411 private fun ensureAllMeasurements() { 412 val mediaStates = mediaHostStatesManager.mediaHostStates 413 for (entry in mediaStates) { 414 obtainViewState(entry.value) 415 } 416 } 417 418 /** Get the constraintSet for a given expansion */ 419 private fun constraintSetForExpansion(expansion: Float): ConstraintSet = 420 if (expansion > 0) expandedLayout else collapsedLayout 421 422 /** Set the height of UMO background constraints. */ 423 private fun setBackgroundHeights(height: Int) { 424 MediaViewHolder.backgroundIds.forEach { id -> 425 expandedLayout.getConstraint(id).layout.mHeight = height 426 } 427 } 428 429 /** 430 * Set the views to be showing/hidden based on the [isGutsVisible] for a given 431 * [TransitionViewState]. 432 */ 433 private fun setGutsViewState(viewState: TransitionViewState) { 434 val controlsIds = MediaViewHolder.controlsIds 435 val gutsIds = GutsViewHolder.ids 436 controlsIds.forEach { id -> 437 viewState.widgetStates.get(id)?.let { state -> 438 // Make sure to use the unmodified state if guts are not visible. 439 state.alpha = if (isGutsVisible) 0f else state.alpha 440 state.gone = if (isGutsVisible) true else state.gone 441 } 442 } 443 gutsIds.forEach { id -> 444 viewState.widgetStates.get(id)?.let { state -> 445 // Make sure to use the unmodified state if guts are visible 446 state.alpha = if (isGutsVisible) state.alpha else 0f 447 state.gone = if (isGutsVisible) state.gone else true 448 } 449 } 450 } 451 452 /** Apply squishFraction to a copy of viewState such that the cached version is untouched. */ 453 internal fun squishViewState( 454 viewState: TransitionViewState, 455 squishFraction: Float, 456 ): TransitionViewState { 457 val squishedViewState = viewState.copy() 458 val squishedHeight = (squishedViewState.measureHeight * squishFraction).toInt() 459 squishedViewState.height = squishedHeight 460 // We are not overriding the squishedViewStates height but only the children to avoid 461 // them remeasuring the whole view. Instead it just remains as the original size 462 MediaViewHolder.backgroundIds.forEach { id -> 463 squishedViewState.widgetStates.get(id)?.let { state -> state.height = squishedHeight } 464 } 465 466 calculateWidgetGroupAlphaForSquishiness( 467 MediaViewHolder.expandedBottomActionIds, 468 squishedViewState.measureHeight.toFloat(), 469 squishedViewState, 470 squishFraction, 471 ) 472 calculateWidgetGroupAlphaForSquishiness( 473 MediaViewHolder.detailIds, 474 squishedViewState.measureHeight.toFloat(), 475 squishedViewState, 476 squishFraction, 477 ) 478 return squishedViewState 479 } 480 481 /** 482 * This function is to make each widget in UMO disappear before being clipped by squished UMO 483 * 484 * The general rule is that widgets in UMO has been divided into several groups, and widgets in 485 * one group have the same alpha during squishing It will change from alpha 0.0 when the visible 486 * bottom of UMO reach the bottom of this group It will change to alpha 1.0 when the visible 487 * bottom of UMO reach the top of the group below e.g.Album title, artist title and play-pause 488 * button will change alpha together. 489 * 490 * ``` 491 * And their alpha becomes 1.0 when the visible bottom of UMO reach the top of controls, 492 * including progress bar, next button, previous button 493 * ``` 494 * 495 * widgetGroupIds: a group of widgets have same state during UMO is squished, 496 * ``` 497 * e.g. Album title, artist title and play-pause button 498 * ``` 499 * 500 * groupEndPosition: the height of UMO, when the height reaches this value, 501 * ``` 502 * widgets in this group should have 1.0 as alpha 503 * e.g., the group of album title, artist title and play-pause button will become fully 504 * visible when the height of UMO reaches the top of controls group 505 * (progress bar, previous button and next button) 506 * ``` 507 * 508 * squishedViewState: hold the widgetState of each widget, which will be modified 509 * squishFraction: the squishFraction of UMO 510 */ 511 private fun calculateWidgetGroupAlphaForSquishiness( 512 widgetGroupIds: Set<Int>, 513 groupEndPosition: Float, 514 squishedViewState: TransitionViewState, 515 squishFraction: Float, 516 ): Float { 517 val nonsquishedHeight = squishedViewState.measureHeight 518 var groupTop = squishedViewState.measureHeight.toFloat() 519 var groupBottom = 0F 520 widgetGroupIds.forEach { id -> 521 squishedViewState.widgetStates.get(id)?.let { state -> 522 groupTop = min(groupTop, state.y) 523 groupBottom = max(groupBottom, state.y + state.height) 524 } 525 } 526 // startPosition means to the height of squished UMO where the widget alpha should start 527 // changing from 0.0 528 // generally, it equals to the bottom of widgets, so that we can meet the requirement that 529 // widget should not go beyond the bounds of background 530 // endPosition means to the height of squished UMO where the widget alpha should finish 531 // changing alpha to 1.0 532 var startPosition = groupBottom 533 val endPosition = groupEndPosition 534 if (startPosition == endPosition) { 535 startPosition = (endPosition - 0.2 * (groupBottom - groupTop)).toFloat() 536 } 537 widgetGroupIds.forEach { id -> 538 squishedViewState.widgetStates.get(id)?.let { state -> 539 // Don't modify alpha for elements that should be invisible (e.g. disabled seekbar) 540 if (state.alpha != 0f) { 541 state.alpha = 542 calculateAlpha( 543 squishFraction, 544 startPosition / nonsquishedHeight, 545 endPosition / nonsquishedHeight, 546 ) 547 } 548 } 549 } 550 return groupTop // used for the widget group above this group 551 } 552 553 /** 554 * Obtain a new viewState for a given media state. This usually returns a cached state, but if 555 * it's not available, it will recreate one by measuring, which may be expensive. 556 */ 557 @VisibleForTesting 558 fun obtainViewState( 559 state: MediaHostState?, 560 isGutsAnimation: Boolean = false, 561 ): TransitionViewState? { 562 if (SceneContainerFlag.isEnabled) { 563 return obtainSceneContainerViewState(state) 564 } 565 566 if (state == null || state.measurementInput == null) { 567 return null 568 } 569 // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey 570 var cacheKey = getKey(state, isGutsVisible, tmpKey) 571 val viewState = viewStates[cacheKey] 572 if (viewState != null) { 573 // we already have cached this measurement, let's continue 574 if (state.squishFraction <= 1f && !isGutsAnimation) { 575 return squishViewState(viewState, state.squishFraction) 576 } 577 return viewState 578 } 579 // Copy the key since this might call recursively into it and we're using tmpKey 580 cacheKey = cacheKey.copy() 581 val result: TransitionViewState? 582 583 if (transitionLayout == null) { 584 return null 585 } 586 // Let's create a new measurement 587 if (state.expansion == 0.0f || state.expansion == 1.0f) { 588 if (state.expansion == 1.0f) { 589 val height = 590 if (state.expandedMatchesParentHeight) { 591 MATCH_CONSTRAINT 592 } else { 593 context.resources.getDimensionPixelSize( 594 R.dimen.qs_media_session_height_expanded 595 ) 596 } 597 setBackgroundHeights(height) 598 } 599 600 result = 601 transitionLayout!!.calculateViewState( 602 state.measurementInput!!, 603 constraintSetForExpansion(state.expansion), 604 TransitionViewState(), 605 ) 606 607 setGutsViewState(result) 608 // We don't want to cache interpolated or null states as this could quickly fill up 609 // our cache. We only cache the start and the end states since the interpolation 610 // is cheap 611 viewStates[cacheKey] = result 612 } else { 613 // This is an interpolated state 614 val startState = state.copy().also { it.expansion = 0.0f } 615 616 // Given that we have a measurement and a view, let's get (guaranteed) viewstates 617 // from the start and end state and interpolate them 618 val startViewState = obtainViewState(startState, isGutsAnimation) as TransitionViewState 619 val endState = state.copy().also { it.expansion = 1.0f } 620 val endViewState = obtainViewState(endState, isGutsAnimation) as TransitionViewState 621 result = 622 layoutController.getInterpolatedState(startViewState, endViewState, state.expansion) 623 } 624 // Skip the adjustments of squish view state if UMO changes due to guts animation. 625 if (state.squishFraction <= 1f && !isGutsAnimation) { 626 return squishViewState(result, state.squishFraction) 627 } 628 return result 629 } 630 631 private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey { 632 result.apply { 633 heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0 634 widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0 635 expansion = state.expansion 636 gutsVisible = guts 637 } 638 return result 639 } 640 641 /** 642 * Attach a view to this controller. This may perform measurements if it's not available yet and 643 * should therefore be done carefully. 644 */ 645 fun attach(transitionLayout: TransitionLayout) = 646 traceSection("MediaViewController#attach") { 647 loadLayoutConstraints() 648 logger.logMediaLocation("attach", currentStartLocation, currentEndLocation) 649 this.transitionLayout = transitionLayout 650 layoutController.attach(transitionLayout) 651 if (currentEndLocation == MediaHierarchyManager.LOCATION_UNKNOWN) { 652 return 653 } 654 // Set the previously set state immediately to the view, now that it's finally attached 655 setCurrentState( 656 startLocation = currentStartLocation, 657 endLocation = currentEndLocation, 658 transitionProgress = currentTransitionProgress, 659 applyImmediately = true, 660 ) 661 } 662 663 fun attachPlayer(mediaViewHolder: MediaViewHolder) { 664 if (!SceneContainerFlag.isEnabled) return 665 this.mediaViewHolder = mediaViewHolder 666 667 // Setting up seek bar. 668 seekBarObserver = SeekBarObserver(mediaViewHolder) 669 seekBarViewModel.progress.observeForever(seekBarObserver) 670 seekBarViewModel.attachTouchHandlers(mediaViewHolder.seekBar) 671 seekBarViewModel.setScrubbingChangeListener(scrubbingChangeListener) 672 seekBarViewModel.setEnabledChangeListener(enabledChangeListener) 673 seekBarViewModel.setContentDescriptionListener(seekbarDescriptionListener) 674 675 val mediaCard = mediaViewHolder.player 676 attach(mediaViewHolder.player) 677 678 val turbulenceNoiseView = mediaViewHolder.turbulenceNoiseView 679 turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView) 680 681 multiRippleController = MultiRippleController(mediaViewHolder.multiRippleView) 682 683 // Metadata Animation 684 val titleText = mediaViewHolder.titleText 685 val artistText = mediaViewHolder.artistText 686 val explicitIndicator = mediaViewHolder.explicitIndicator 687 val enter = 688 loadAnimator( 689 mediaCard.context, 690 R.anim.media_metadata_enter, 691 Interpolators.EMPHASIZED_DECELERATE, 692 titleText, 693 artistText, 694 explicitIndicator, 695 ) 696 val exit = 697 loadAnimator( 698 mediaCard.context, 699 R.anim.media_metadata_exit, 700 Interpolators.EMPHASIZED_ACCELERATE, 701 titleText, 702 artistText, 703 explicitIndicator, 704 ) 705 metadataAnimationHandler = MetadataAnimationHandler(exit, enter) 706 707 colorSchemeTransition = 708 ColorSchemeTransition( 709 mediaCard.context, 710 mediaViewHolder, 711 multiRippleController, 712 turbulenceNoiseController, 713 ) 714 715 // For Turbulence noise. 716 val loadingEffectView = mediaViewHolder.loadingEffectView 717 noiseDrawCallback = 718 object : PaintDrawCallback { 719 override fun onDraw(paint: Paint) { 720 loadingEffectView.draw(paint) 721 } 722 } 723 stateChangedCallback = 724 object : LoadingEffect.AnimationStateChangedCallback { 725 override fun onStateChanged( 726 oldState: LoadingEffect.AnimationState, 727 newState: LoadingEffect.AnimationState, 728 ) { 729 if (newState === LoadingEffect.AnimationState.NOT_PLAYING) { 730 loadingEffectView.visibility = View.INVISIBLE 731 } else { 732 loadingEffectView.visibility = View.VISIBLE 733 } 734 } 735 } 736 } 737 738 fun updateAnimatorDurationScale() { 739 if (!SceneContainerFlag.isEnabled) return 740 if (this::seekBarObserver.isInitialized) { 741 seekBarObserver.animationEnabled = 742 globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f 743 } 744 } 745 746 /** update view with the needed UI changes when user touches seekbar. */ 747 private fun updateDisplayForScrubbingChange() { 748 mainExecutor.execute { 749 val isTimeVisible = canShowScrubbingTime && isScrubbing 750 mediaViewHolder!!.let { 751 MediaControlViewBinder.setVisibleAndAlpha( 752 expandedLayout, 753 it.scrubbingTotalTimeView.id, 754 isTimeVisible, 755 ) 756 MediaControlViewBinder.setVisibleAndAlpha( 757 expandedLayout, 758 it.scrubbingElapsedTimeView.id, 759 isTimeVisible, 760 ) 761 } 762 763 MediaControlViewModel.SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach { id -> 764 val isButtonVisible: Boolean 765 val notVisibleValue: Int 766 when (id) { 767 R.id.actionPrev -> { 768 isButtonVisible = isPrevButtonAvailable && !isTimeVisible 769 notVisibleValue = prevNotVisibleValue 770 } 771 R.id.actionNext -> { 772 isButtonVisible = isNextButtonAvailable && !isTimeVisible 773 notVisibleValue = nextNotVisibleValue 774 } 775 else -> { 776 isButtonVisible = !isTimeVisible 777 notVisibleValue = ConstraintSet.GONE 778 } 779 } 780 mediaViewHolder!!.let { 781 MediaControlViewBinder.setSemanticButtonVisibleAndAlpha( 782 it.getAction(id), 783 expandedLayout, 784 collapsedLayout, 785 isButtonVisible, 786 notVisibleValue, 787 showInCollapsed = true, 788 ) 789 } 790 } 791 792 if (!metadataAnimationHandler.isRunning) { 793 refreshState() 794 } 795 } 796 } 797 798 fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) { 799 if (!SceneContainerFlag.isEnabled) return 800 seekBarViewModel.logSeek = onSeek 801 onBindSeekBar(seekBarViewModel) 802 } 803 804 fun setUpTurbulenceNoise() { 805 if (!SceneContainerFlag.isEnabled) return 806 mediaViewHolder!!.let { 807 if (!this::turbulenceNoiseAnimationConfig.isInitialized) { 808 turbulenceNoiseAnimationConfig = 809 createTurbulenceNoiseConfig( 810 it.loadingEffectView, 811 it.turbulenceNoiseView, 812 colorSchemeTransition, 813 ) 814 } 815 if (Flags.shaderlibLoadingEffectRefactor()) { 816 if (!this::loadingEffect.isInitialized) { 817 loadingEffect = 818 LoadingEffect( 819 TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, 820 turbulenceNoiseAnimationConfig, 821 noiseDrawCallback, 822 stateChangedCallback, 823 ) 824 } 825 colorSchemeTransition.loadingEffect = loadingEffect 826 loadingEffect.play() 827 mainExecutor.executeDelayed( 828 loadingEffect::finish, 829 MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION, 830 ) 831 } else { 832 turbulenceNoiseController.play( 833 TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, 834 turbulenceNoiseAnimationConfig, 835 ) 836 mainExecutor.executeDelayed( 837 turbulenceNoiseController::finish, 838 MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION, 839 ) 840 } 841 } 842 } 843 844 /** 845 * Obtain a measurement for a given location. This makes sure that the state is up to date and 846 * all widgets know their location. Calling this method may create a measurement if we don't 847 * have a cached value available already. 848 */ 849 fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? = 850 traceSection("MediaViewController#getMeasurementsForState") { 851 // measurements should never factor in the squish fraction 852 val viewState = obtainViewState(hostState) ?: return null 853 measurement.measuredWidth = viewState.measureWidth 854 measurement.measuredHeight = viewState.measureHeight 855 return measurement 856 } 857 858 /** 859 * Set a new state for the controlled view which can be an interpolation between multiple 860 * locations. 861 */ 862 fun setCurrentState( 863 @MediaLocation startLocation: Int, 864 @MediaLocation endLocation: Int, 865 transitionProgress: Float, 866 applyImmediately: Boolean, 867 isGutsAnimation: Boolean = false, 868 ) = 869 traceSection("MediaViewController#setCurrentState") { 870 currentEndLocation = endLocation 871 currentStartLocation = startLocation 872 currentTransitionProgress = transitionProgress 873 874 val shouldAnimate = animateNextStateChange && !applyImmediately 875 876 val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return 877 val startHostState = mediaHostStatesManager.mediaHostStates[startLocation] 878 879 // Obtain the view state that we'd want to be at the end 880 // The view might not be bound yet or has never been measured and in that case will be 881 // reset once the state is fully available 882 var endViewState = obtainViewState(endHostState, isGutsAnimation) ?: return 883 endViewState = updateViewStateSize(endViewState, endLocation, tmpState2)!! 884 layoutController.setMeasureState(endViewState) 885 886 // If the view isn't bound, we can drop the animation, otherwise we'll execute it 887 animateNextStateChange = false 888 if (transitionLayout == null) { 889 logger.logMediaLocation( 890 "setCurrentState: view not bound", 891 startLocation, 892 endLocation, 893 ) 894 return 895 } 896 897 val result: TransitionViewState 898 var startViewState = obtainViewState(startHostState, isGutsAnimation) 899 startViewState = updateViewStateSize(startViewState, startLocation, tmpState3) 900 901 if (!endHostState.visible) { 902 // Let's handle the case where the end is gone first. In this case we take the 903 // start viewState and will make it gone 904 if (startViewState == null || startHostState == null || !startHostState.visible) { 905 // the start isn't a valid state, let's use the endstate directly 906 result = endViewState 907 } else { 908 // Let's get the gone presentation from the start state 909 result = 910 layoutController.getGoneState( 911 startViewState, 912 startHostState.disappearParameters, 913 transitionProgress, 914 tmpState, 915 ) 916 } 917 } else if (startHostState != null && !startHostState.visible) { 918 // We have a start state and it is gone. 919 // Let's get presentation from the endState 920 result = 921 layoutController.getGoneState( 922 endViewState, 923 endHostState.disappearParameters, 924 1.0f - transitionProgress, 925 tmpState, 926 ) 927 } else if (transitionProgress == 1.0f || startViewState == null) { 928 // We're at the end. Let's use that state 929 result = endViewState 930 } else if (transitionProgress == 0.0f) { 931 // We're at the start. Let's use that state 932 result = startViewState 933 } else { 934 result = 935 layoutController.getInterpolatedState( 936 startViewState, 937 endViewState, 938 transitionProgress, 939 tmpState, 940 ) 941 } 942 logger.logMediaSize( 943 "setCurrentState $startLocation -> $endLocation (progress $transitionProgress)", 944 result.width, 945 result.height, 946 ) 947 layoutController.setState( 948 result, 949 applyImmediately, 950 shouldAnimate, 951 animationDuration, 952 animationDelay, 953 isGutsAnimation, 954 ) 955 } 956 957 private fun updateViewStateSize( 958 viewState: TransitionViewState?, 959 location: Int, 960 outState: TransitionViewState, 961 ): TransitionViewState? { 962 var result = viewState?.copy(outState) ?: return null 963 val state = mediaHostStatesManager.mediaHostStates[location] 964 val overrideSize = mediaHostStatesManager.carouselSizes[location] 965 var overridden = false 966 overrideSize?.let { 967 if (SceneContainerFlag.isEnabled) { 968 result.measureWidth = widthInSceneContainerPx 969 result.measureHeight = heightInSceneContainerPx 970 overridden = true 971 } else if ( 972 result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth 973 ) { 974 // To be safe we're using a maximum here. The override size should always be set 975 // properly though. 976 result.measureHeight = Math.max(it.measuredHeight, result.measureHeight) 977 result.measureWidth = Math.max(it.measuredWidth, result.measureWidth) 978 overridden = true 979 } 980 if (overridden) { 981 // The measureHeight and the shown height should both be set to the overridden 982 // height 983 result.height = result.measureHeight 984 result.width = result.measureWidth 985 // Make sure all background views are also resized such that their size is correct 986 MediaViewHolder.backgroundIds.forEach { id -> 987 result.widgetStates.get(id)?.let { state -> 988 state.height = result.height 989 state.width = result.width 990 } 991 } 992 } 993 } 994 if (overridden && state != null && state.squishFraction <= 1f) { 995 // Let's squish the media player if our size was overridden 996 result = squishViewState(result, state.squishFraction) 997 } 998 logger.logMediaSize("update to carousel", result.width, result.height) 999 return result 1000 } 1001 1002 private fun loadLayoutConstraints() { 1003 // These XML resources contain ConstraintSets that will apply to this player's layout 1004 collapsedLayout.load(context, R.xml.media_session_collapsed) 1005 expandedLayout.load(context, R.xml.media_session_expanded) 1006 readjustUIUpdateConstraints() 1007 refreshState() 1008 } 1009 1010 private fun readjustUIUpdateConstraints() { 1011 // TODO: move to xml file when flag is removed. 1012 if (Flags.mediaControlsUiUpdate()) { 1013 collapsedLayout.setGuidelineEnd( 1014 R.id.action_button_guideline, 1015 context.resources.getDimensionPixelSize( 1016 R.dimen.qs_media_session_collapsed_guideline 1017 ), 1018 ) 1019 collapsedLayout.constrainWidth( 1020 R.id.actionPlayPause, 1021 context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), 1022 ) 1023 expandedLayout.constrainWidth( 1024 R.id.actionPlayPause, 1025 context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), 1026 ) 1027 } 1028 } 1029 1030 /** Get a view state based on the width and height set by the scene */ 1031 private fun obtainSceneContainerViewState(state: MediaHostState?): TransitionViewState? { 1032 logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx) 1033 1034 if (state?.measurementInput == null) { 1035 return null 1036 } 1037 1038 if (state.expansion == 1.0f) { 1039 val height = 1040 if (state.expandedMatchesParentHeight) { 1041 heightInSceneContainerPx 1042 } else { 1043 context.resources.getDimensionPixelSize( 1044 R.dimen.qs_media_session_height_expanded 1045 ) 1046 } 1047 setBackgroundHeights(height) 1048 } 1049 1050 // Similar to obtainViewState: Let's create a new measurement 1051 val result = 1052 transitionLayout?.calculateViewState( 1053 MeasurementInput(widthInSceneContainerPx, heightInSceneContainerPx), 1054 if (state.expansion > 0) expandedLayout else collapsedLayout, 1055 TransitionViewState(), 1056 ) 1057 result?.let { 1058 // And then ensure the guts visibility is set correctly 1059 setGutsViewState(it) 1060 } 1061 return result 1062 } 1063 1064 /** 1065 * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event 1066 * of [location] not being visible, [locationWhenHidden] will be used instead. 1067 * 1068 * @param location Target 1069 * @param locationWhenHidden Location that will be used when the target is not 1070 * [MediaHost.visible] 1071 * @return State require for executing a transition, and also the respective [MediaHost]. 1072 */ 1073 private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? { 1074 val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null 1075 if (SceneContainerFlag.isEnabled) { 1076 return obtainSceneContainerViewState(mediaHostState) 1077 } 1078 1079 val viewState = obtainViewState(mediaHostState) 1080 if (viewState != null) { 1081 // update the size of the viewstate for the location with the override 1082 updateViewStateSize(viewState, location, tmpState) 1083 return tmpState 1084 } 1085 return viewState 1086 } 1087 1088 private fun updateFontPerLocation(viewHolder: MediaViewHolder?, location: Int) { 1089 when (location) { 1090 MediaHierarchyManager.LOCATION_COMMUNAL_HUB -> 1091 viewHolder?.updateFontFamily(headlineSmallTF, titleMediumTF, labelMediumTF) 1092 else -> viewHolder?.updateFontFamily(titleMediumTF, labelLargeTF, labelMediumTF) 1093 } 1094 } 1095 1096 private fun MediaViewHolder.updateFontFamily( 1097 titleTF: Typeface, 1098 artistTF: Typeface, 1099 menuTF: Typeface, 1100 ) { 1101 gutsViewHolder.gutsText.setTypeface(menuTF) 1102 gutsViewHolder.dismissText.setTypeface(menuTF) 1103 gutsViewHolder.cancelText.setTypeface(menuTF) 1104 titleText.setTypeface(titleTF) 1105 artistText.setTypeface(artistTF) 1106 seamlessText.setTypeface(menuTF) 1107 } 1108 1109 /** 1110 * Notify that the location is changing right now and a [setCurrentState] change is imminent. 1111 * This updates the width the view will me measured with. 1112 */ 1113 fun onLocationPreChange( 1114 viewHolder: MediaViewHolder?, 1115 @MediaLocation newLocation: Int, 1116 @MediaLocation prevLocation: Int, 1117 ) { 1118 isFontUpdateAllowed = 1119 isFontUpdateAllowed || 1120 MediaHierarchyManager.LOCATION_COMMUNAL_HUB == newLocation || 1121 MediaHierarchyManager.LOCATION_COMMUNAL_HUB == prevLocation 1122 if (Flags.mediaControlsUiUpdate() && isFontUpdateAllowed) { 1123 updateFontPerLocation(viewHolder, newLocation) 1124 isFontUpdateAllowed = false 1125 } 1126 obtainViewStateForLocation(newLocation)?.let { layoutController.setMeasureState(it) } 1127 } 1128 1129 /** Request that the next state change should be animated with the given parameters. */ 1130 fun animatePendingStateChange(duration: Long, delay: Long) { 1131 animateNextStateChange = true 1132 animationDuration = duration 1133 animationDelay = delay 1134 } 1135 1136 /** Clear all existing measurements and refresh the state to match the view. */ 1137 fun refreshState() = 1138 traceSection("MediaViewController#refreshState") { 1139 if (SceneContainerFlag.isEnabled) { 1140 val hostState = mediaHostStatesManager.mediaHostStates[currentEndLocation] 1141 // We don't need to recreate measurements for scene container, since it's a known 1142 // size. Just get the view state and update the layout controller 1143 obtainSceneContainerViewState(hostState)?.let { 1144 // Get scene container state, then setCurrentState 1145 layoutController.setState( 1146 state = it, 1147 applyImmediately = true, 1148 animate = false, 1149 isGuts = false, 1150 ) 1151 } 1152 return 1153 } 1154 1155 // Let's clear all of our measurements and recreate them! 1156 viewStates.clear() 1157 if (firstRefresh) { 1158 // This is the first bind, let's ensure we pre-cache all measurements. Otherwise 1159 // We'll just load these on demand. 1160 ensureAllMeasurements() 1161 firstRefresh = false 1162 } 1163 setCurrentState( 1164 currentStartLocation, 1165 currentEndLocation, 1166 currentTransitionProgress, 1167 applyImmediately = true, 1168 ) 1169 } 1170 1171 @VisibleForTesting 1172 protected open fun loadAnimator( 1173 context: Context, 1174 animId: Int, 1175 motionInterpolator: Interpolator?, 1176 vararg targets: View?, 1177 ): AnimatorSet { 1178 val animators = ArrayList<Animator>() 1179 for (target in targets) { 1180 val animator = AnimatorInflater.loadAnimator(context, animId) as AnimatorSet 1181 animator.childAnimations[0].interpolator = motionInterpolator 1182 animator.setTarget(target) 1183 animators.add(animator) 1184 } 1185 val result = AnimatorSet() 1186 result.playTogether(animators) 1187 return result 1188 } 1189 1190 private fun createTurbulenceNoiseConfig( 1191 loadingEffectView: LoadingEffectView, 1192 turbulenceNoiseView: TurbulenceNoiseView, 1193 colorSchemeTransition: ColorSchemeTransition, 1194 ): TurbulenceNoiseAnimationConfig { 1195 val targetView: View = 1196 if (Flags.shaderlibLoadingEffectRefactor()) { 1197 loadingEffectView 1198 } else { 1199 turbulenceNoiseView 1200 } 1201 val width = targetView.width 1202 val height = targetView.height 1203 val random = Random() 1204 val luminosity = 1205 if (Flags.mediaControlsA11yColors()) { 1206 0.6f 1207 } else { 1208 TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER 1209 } 1210 return TurbulenceNoiseAnimationConfig( 1211 gridCount = 2.14f, 1212 luminosity, 1213 random.nextFloat(), 1214 random.nextFloat(), 1215 random.nextFloat(), 1216 noiseMoveSpeedX = 0.42f, 1217 noiseMoveSpeedY = 0f, 1218 TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, 1219 // Color will be correctly updated in ColorSchemeTransition. 1220 colorSchemeTransition.getSurfaceEffectColor(), 1221 screenColor = Color.BLACK, 1222 width.toFloat(), 1223 height.toFloat(), 1224 TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS, 1225 easeInDuration = 1350f, 1226 easeOutDuration = 1350f, 1227 targetView.context.resources.displayMetrics.density, 1228 lumaMatteBlendFactor = 0.26f, 1229 lumaMatteOverallBrightness = 0.09f, 1230 shouldInverseNoiseLuminosity = false, 1231 ) 1232 } 1233 1234 fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { 1235 if (!SceneContainerFlag.isEnabled) return 1236 isPrevButtonAvailable = isAvailable 1237 prevNotVisibleValue = notVisibleValue 1238 } 1239 1240 fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { 1241 if (!SceneContainerFlag.isEnabled) return 1242 isNextButtonAvailable = isAvailable 1243 nextNotVisibleValue = notVisibleValue 1244 } 1245 } 1246 1247 /** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */ 1248 private data class CacheKey( 1249 var widthMeasureSpec: Int = -1, 1250 var heightMeasureSpec: Int = -1, 1251 var expansion: Float = 0.0f, 1252 var gutsVisible: Boolean = false, 1253 ) 1254