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