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