1 /* <lambda>null2 * Copyright (C) 2022 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.annotation.WorkerThread 20 import android.app.PendingIntent 21 import android.content.Context 22 import android.content.Intent 23 import android.content.res.ColorStateList 24 import android.content.res.Configuration 25 import android.database.ContentObserver 26 import android.os.UserHandle 27 import android.provider.Settings 28 import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS 29 import android.util.Log 30 import android.util.MathUtils 31 import android.view.LayoutInflater 32 import android.view.View 33 import android.view.ViewGroup 34 import android.view.animation.PathInterpolator 35 import android.widget.ImageView 36 import android.widget.LinearLayout 37 import androidx.annotation.VisibleForTesting 38 import androidx.lifecycle.Lifecycle 39 import androidx.lifecycle.repeatOnLifecycle 40 import androidx.recyclerview.widget.DiffUtil 41 import com.android.app.tracing.coroutines.launchTraced as launch 42 import com.android.app.tracing.traceSection 43 import com.android.internal.logging.InstanceId 44 import com.android.keyguard.KeyguardUpdateMonitor 45 import com.android.keyguard.KeyguardUpdateMonitorCallback 46 import com.android.systemui.Dumpable 47 import com.android.systemui.Flags.mediaControlsUmoInflationInBackground 48 import com.android.systemui.dagger.SysUISingleton 49 import com.android.systemui.dagger.qualifiers.Application 50 import com.android.systemui.dagger.qualifiers.Background 51 import com.android.systemui.dagger.qualifiers.Main 52 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor 53 import com.android.systemui.dump.DumpManager 54 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 55 import com.android.systemui.keyguard.shared.model.Edge 56 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING 57 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE 58 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN 59 import com.android.systemui.keyguard.shared.model.TransitionState 60 import com.android.systemui.lifecycle.repeatWhenAttached 61 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager 62 import com.android.systemui.media.controls.shared.model.MediaData 63 import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder 64 import com.android.systemui.media.controls.ui.util.MediaViewModelCallback 65 import com.android.systemui.media.controls.ui.util.MediaViewModelListUpdateCallback 66 import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler 67 import com.android.systemui.media.controls.ui.view.MediaHostState 68 import com.android.systemui.media.controls.ui.view.MediaScrollView 69 import com.android.systemui.media.controls.ui.view.MediaViewHolder 70 import com.android.systemui.media.controls.ui.viewmodel.MediaCarouselViewModel 71 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel 72 import com.android.systemui.media.controls.util.MediaUiEventLogger 73 import com.android.systemui.plugins.ActivityStarter 74 import com.android.systemui.plugins.FalsingManager 75 import com.android.systemui.qs.PageIndicator 76 import com.android.systemui.res.R 77 import com.android.systemui.scene.shared.flag.SceneContainerFlag 78 import com.android.systemui.scene.shared.model.Scenes 79 import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor 80 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener 81 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider 82 import com.android.systemui.statusbar.policy.ConfigurationController 83 import com.android.systemui.util.Utils 84 import com.android.systemui.util.animation.UniqueObjectHostView 85 import com.android.systemui.util.animation.requiresRemeasuring 86 import com.android.systemui.util.concurrency.DelayableExecutor 87 import com.android.systemui.util.settings.GlobalSettings 88 import com.android.systemui.util.settings.SecureSettings 89 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow 90 import com.android.systemui.util.time.SystemClock 91 import java.io.PrintWriter 92 import java.util.Locale 93 import java.util.TreeMap 94 import java.util.concurrent.Executor 95 import javax.inject.Inject 96 import javax.inject.Provider 97 import kotlinx.coroutines.CoroutineDispatcher 98 import kotlinx.coroutines.CoroutineScope 99 import kotlinx.coroutines.Job 100 import kotlinx.coroutines.flow.SharingStarted 101 import kotlinx.coroutines.flow.collectLatest 102 import kotlinx.coroutines.flow.distinctUntilChanged 103 import kotlinx.coroutines.flow.filter 104 import kotlinx.coroutines.flow.flowOn 105 import kotlinx.coroutines.flow.map 106 import kotlinx.coroutines.flow.merge 107 import kotlinx.coroutines.flow.onStart 108 import kotlinx.coroutines.flow.stateIn 109 import kotlinx.coroutines.withContext 110 111 private const val TAG = "MediaCarouselController" 112 private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) 113 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) 114 115 /** 116 * Class that is responsible for keeping the view carousel up to date. This also handles changes in 117 * state and applies them to the media carousel like the expansion. 118 */ 119 @SysUISingleton 120 class MediaCarouselController 121 @Inject 122 constructor( 123 @Application applicationScope: CoroutineScope, 124 @Main private val context: Context, 125 private val mediaControlPanelFactory: Provider<MediaControlPanel>, 126 private val visualStabilityProvider: VisualStabilityProvider, 127 private val mediaHostStatesManager: MediaHostStatesManager, 128 private val activityStarter: ActivityStarter, 129 private val systemClock: SystemClock, 130 @Main private val mainDispatcher: CoroutineDispatcher, 131 @Main private val uiExecutor: DelayableExecutor, 132 @Background private val bgExecutor: Executor, 133 @Background private val backgroundDispatcher: CoroutineDispatcher, 134 private val mediaManager: MediaDataManager, 135 @Main configurationController: ConfigurationController, 136 private val falsingManager: FalsingManager, 137 dumpManager: DumpManager, 138 private val logger: MediaUiEventLogger, 139 private val debugLogger: MediaCarouselControllerLogger, 140 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 141 private val keyguardTransitionInteractor: KeyguardTransitionInteractor, 142 private val globalSettings: GlobalSettings, 143 private val secureSettings: SecureSettings, 144 private val mediaCarouselViewModel: MediaCarouselViewModel, 145 private val mediaViewControllerFactory: Provider<MediaViewController>, 146 private val deviceEntryInteractor: DeviceEntryInteractor, 147 private val mediaControlChipInteractor: MediaControlChipInteractor, 148 ) : Dumpable { 149 /** The current width of the carousel */ 150 var currentCarouselWidth: Int = 0 151 private set 152 153 /** The current height of the carousel */ 154 private var currentCarouselHeight: Int = 0 155 156 /** Are we currently showing only active players */ 157 private var currentlyShowingOnlyActive: Boolean = false 158 159 /** Is the player currently visible (at the end of the transformation */ 160 private var playersVisible: Boolean = false 161 162 /** Are we currently disabling scolling, only allowing the first media session to show */ 163 private var currentlyDisableScrolling: Boolean = false 164 165 /** 166 * The desired location where we'll be at the end of the transformation. Usually this matches 167 * the end location, except when we're still waiting on a state update call. 168 */ 169 @MediaLocation private var desiredLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN 170 171 /** 172 * The ending location of the view where it ends when all animations and transitions have 173 * finished 174 */ 175 @MediaLocation 176 @VisibleForTesting 177 var currentEndLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN 178 179 /** 180 * The ending location of the view where it ends when all animations and transitions have 181 * finished 182 */ 183 @MediaLocation private var currentStartLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN 184 185 /** The progress of the transition or 1.0 if there is no transition happening */ 186 private var currentTransitionProgress: Float = 1.0f 187 188 /** The measured width of the carousel */ 189 private var carouselMeasureWidth: Int = 0 190 191 /** The measured height of the carousel */ 192 private var carouselMeasureHeight: Int = 0 193 private var desiredHostState: MediaHostState? = null 194 @VisibleForTesting var mediaCarousel: MediaScrollView 195 val mediaCarouselScrollHandler: MediaCarouselScrollHandler 196 val mediaFrame: ViewGroup 197 198 @VisibleForTesting 199 lateinit var settingsButton: ImageView 200 private set 201 202 private val mediaContent: ViewGroup 203 @VisibleForTesting var pageIndicator: PageIndicator 204 private var needsReordering: Boolean = false 205 private var isUserInitiatedRemovalQueued: Boolean = false 206 private var keysNeedRemoval = mutableSetOf<String>() 207 private var isRtl: Boolean = false 208 set(value) { 209 if (value != field) { 210 field = value 211 mediaFrame.layoutDirection = 212 if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR 213 mediaCarouselScrollHandler.scrollToStart() 214 } 215 } 216 217 private var carouselLocale: Locale? = null 218 219 private val animationScaleObserver: ContentObserver = 220 object : ContentObserver(uiExecutor, 0) { 221 override fun onChange(selfChange: Boolean) { 222 if (!SceneContainerFlag.isEnabled) { 223 MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() } 224 } else { 225 controllerById.values.forEach { it.updateAnimatorDurationScale() } 226 } 227 } 228 } 229 230 private var allowMediaPlayerOnLockScreen = false 231 232 /** Whether the media card currently has the "expanded" layout */ 233 @VisibleForTesting 234 var currentlyExpanded = true 235 set(value) { 236 if (field != value) { 237 field = value 238 updateSeekbarListening(mediaCarouselScrollHandler.visibleToUser) 239 } 240 } 241 242 companion object { 243 val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F) 244 245 fun calculateAlpha( 246 squishinessFraction: Float, 247 startPosition: Float, 248 endPosition: Float, 249 ): Float { 250 val transformFraction = 251 MathUtils.constrain( 252 (squishinessFraction - startPosition) / (endPosition - startPosition), 253 0F, 254 1F, 255 ) 256 return TRANSFORM_BEZIER.getInterpolation(transformFraction) 257 } 258 } 259 260 private val configListener = 261 object : ConfigurationController.ConfigurationListener { 262 263 override fun onDensityOrFontScaleChanged() { 264 // System font changes should only happen when UMO is offscreen or a flicker may 265 // occur 266 updatePlayers(recreateMedia = true) 267 inflateSettingsButton() 268 } 269 270 override fun onThemeChanged() { 271 updatePlayers(recreateMedia = false) 272 inflateSettingsButton() 273 } 274 275 override fun onConfigChanged(newConfig: Configuration?) { 276 if (newConfig == null) return 277 isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL 278 } 279 280 override fun onUiModeChanged() { 281 updatePlayers(recreateMedia = false) 282 inflateSettingsButton() 283 } 284 285 override fun onLocaleListChanged() { 286 // Update players only if system primary language changes. 287 if (carouselLocale != context.resources.configuration.locales.get(0)) { 288 carouselLocale = context.resources.configuration.locales.get(0) 289 updatePlayers(recreateMedia = true) 290 inflateSettingsButton() 291 } 292 } 293 } 294 295 private val keyguardUpdateMonitorCallback = 296 object : KeyguardUpdateMonitorCallback() { 297 override fun onStrongAuthStateChanged(userId: Int) { 298 if (keyguardUpdateMonitor.isUserInLockdown(userId)) { 299 debugLogger.logCarouselHidden() 300 hideMediaCarousel() 301 } else if (keyguardUpdateMonitor.isUserUnlocked(userId)) { 302 debugLogger.logCarouselVisible() 303 showMediaCarousel() 304 } 305 } 306 } 307 308 /** 309 * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility. 310 * It will be called when the container is out of view. 311 */ 312 lateinit var updateUserVisibility: () -> Unit 313 var updateHostVisibility: () -> Unit = {} 314 set(value) { 315 field = value 316 mediaCarouselViewModel.updateHostVisibility = value 317 } 318 319 private val isReorderingAllowed: Boolean 320 get() = visualStabilityProvider.isReorderingAllowed 321 322 /** Size provided by the scene framework container */ 323 private var widthInSceneContainerPx = 0 324 private var heightInSceneContainerPx = 0 325 326 private val controllerById = mutableMapOf<InstanceId, MediaViewController>() 327 private val controlViewModels = mutableListOf<MediaControlViewModel>() 328 329 private val isOnGone = 330 keyguardTransitionInteractor 331 .isFinishedIn(Scenes.Gone, GONE) 332 .stateIn(applicationScope, SharingStarted.Eagerly, true) 333 334 private val isGoingToDozing = 335 keyguardTransitionInteractor 336 .isInTransition(Edge.create(to = DOZING)) 337 .stateIn(applicationScope, SharingStarted.Eagerly, true) 338 339 init { 340 dumpManager.registerNormalDumpable(TAG, this) 341 mediaFrame = inflateMediaCarousel() 342 mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) 343 pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) 344 mediaCarouselScrollHandler = 345 MediaCarouselScrollHandler( 346 mediaCarousel, 347 pageIndicator, 348 uiExecutor, 349 this::onSwipeToDismiss, 350 this::updatePageIndicatorLocation, 351 this::updateSeekbarListening, 352 this::closeGuts, 353 falsingManager, 354 logger, 355 ) 356 carouselLocale = context.resources.configuration.locales.get(0) 357 isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL 358 inflateSettingsButton() 359 mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) 360 configurationController.addCallback(configListener) 361 if (!SceneContainerFlag.isEnabled) { 362 setUpListeners() 363 } else { 364 val visualStabilityCallback = OnReorderingAllowedListener { 365 mediaCarouselViewModel.onReorderingAllowed() 366 367 // Update user visibility so that no extra impression will be logged when 368 // activeMediaIndex resets to 0 369 if (this::updateUserVisibility.isInitialized) { 370 updateUserVisibility() 371 } 372 373 // Let's reset our scroll position 374 mediaCarouselScrollHandler.scrollToStart() 375 } 376 visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback) 377 } 378 mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> 379 // The pageIndicator is not laid out yet when we get the current state update, 380 // Lets make sure we have the right dimensions 381 updatePageIndicatorLocation() 382 } 383 mediaHostStatesManager.addCallback( 384 object : MediaHostStatesManager.Callback { 385 override fun onHostStateChanged( 386 @MediaLocation location: Int, 387 mediaHostState: MediaHostState, 388 ) { 389 updateUserVisibility() 390 if (location == desiredLocation) { 391 onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false) 392 } 393 } 394 } 395 ) 396 keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) 397 mediaCarousel.repeatWhenAttached { 398 repeatOnLifecycle(Lifecycle.State.STARTED) { 399 listenForAnyStateToGoneKeyguardTransition(this) 400 listenForAnyStateToLockscreenTransition(this) 401 listenForAnyStateToDozingTransition(this) 402 403 if (!SceneContainerFlag.isEnabled) return@repeatOnLifecycle 404 listenForMediaItemsChanges(this) 405 } 406 } 407 listenForLockscreenSettingChanges(applicationScope) 408 409 // Notifies all active players about animation scale changes. 410 bgExecutor.execute { 411 globalSettings.registerContentObserverSync( 412 Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), 413 animationScaleObserver, 414 ) 415 } 416 } 417 418 private fun setUpListeners() { 419 val visualStabilityCallback = OnReorderingAllowedListener { 420 if (needsReordering) { 421 needsReordering = false 422 reorderAllPlayers(previousVisiblePlayerKey = null) 423 } 424 425 keysNeedRemoval.forEach { 426 removePlayer(it, userInitiated = isUserInitiatedRemovalQueued) 427 } 428 if (keysNeedRemoval.size > 0) { 429 // Carousel visibility may need to be updated after late removals 430 updateHostVisibility() 431 } 432 keysNeedRemoval.clear() 433 isUserInitiatedRemovalQueued = false 434 435 // Update user visibility so that no extra impression will be logged when 436 // activeMediaIndex resets to 0 437 if (this::updateUserVisibility.isInitialized) { 438 updateUserVisibility() 439 } 440 441 // Let's reset our scroll position 442 mediaCarouselScrollHandler.scrollToStart() 443 } 444 visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback) 445 mediaManager.addListener( 446 object : MediaDataManager.Listener { 447 override fun onMediaDataLoaded( 448 key: String, 449 oldKey: String?, 450 data: MediaData, 451 immediately: Boolean, 452 receivedSmartspaceCardLatency: Int, 453 isSsReactivated: Boolean, 454 ) { 455 debugLogger.logMediaLoaded(key, data.active) 456 val onUiExecutionEnd = 457 if (mediaControlsUmoInflationInBackground()) { 458 Runnable { 459 if (immediately) { 460 updateHostVisibility() 461 } 462 } 463 } else { 464 null 465 } 466 addOrUpdatePlayer(key, oldKey, data, onUiExecutionEnd) 467 468 val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active 469 if (canRemove && !Utils.useMediaResumption(context)) { 470 // This media control is both paused and timed out, and the resumption 471 // setting is off - let's remove it 472 if (isReorderingAllowed) { 473 onMediaDataRemoved(key, userInitiated = MediaPlayerData.isSwipedAway) 474 } else { 475 isUserInitiatedRemovalQueued = MediaPlayerData.isSwipedAway 476 keysNeedRemoval.add(key) 477 } 478 } else { 479 keysNeedRemoval.remove(key) 480 } 481 MediaPlayerData.isSwipedAway = false 482 } 483 484 override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { 485 debugLogger.logMediaRemoved(key, userInitiated) 486 removePlayer(key, userInitiated = userInitiated) 487 } 488 } 489 ) 490 } 491 492 private fun inflateSettingsButton() { 493 val settings = 494 LayoutInflater.from(context) 495 .inflate(R.layout.media_carousel_settings_button, mediaFrame, false) as ImageView 496 if (this::settingsButton.isInitialized) { 497 mediaFrame.removeView(settingsButton) 498 } 499 settingsButton = settings 500 mediaFrame.addView(settingsButton) 501 mediaCarouselScrollHandler.onSettingsButtonUpdated(settings) 502 settingsButton.setOnClickListener { 503 logger.logCarouselSettings() 504 activityStarter.startActivity(settingsIntent, /* dismissShade= */ true) 505 } 506 } 507 508 private fun inflateMediaCarousel(): ViewGroup { 509 val mediaCarousel = 510 LayoutInflater.from(context) 511 .inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup 512 // Because this is inflated when not attached to the true view hierarchy, it resolves some 513 // potential issues to force that the layout direction is defined by the locale 514 // (rather than inherited from the parent, which would resolve to LTR when unattached). 515 mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE 516 return mediaCarousel 517 } 518 519 private fun hideMediaCarousel() { 520 mediaCarousel.visibility = View.GONE 521 } 522 523 private fun showMediaCarousel() { 524 mediaCarousel.visibility = View.VISIBLE 525 } 526 527 @VisibleForTesting 528 internal fun listenForAnyStateToGoneKeyguardTransition(scope: CoroutineScope): Job { 529 return scope.launch { 530 keyguardTransitionInteractor 531 .isFinishedIn(content = Scenes.Gone, stateWithoutSceneContainer = GONE) 532 .filter { it } 533 .collect { 534 showMediaCarousel() 535 updateHostVisibility() 536 } 537 } 538 } 539 540 @VisibleForTesting 541 internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job { 542 return scope.launch { 543 keyguardTransitionInteractor 544 .transition(Edge.create(to = LOCKSCREEN)) 545 .filter { it.transitionState == TransitionState.FINISHED } 546 .collect { 547 if (!allowMediaPlayerOnLockScreen) { 548 updateHostVisibility() 549 } 550 } 551 } 552 } 553 554 @VisibleForTesting 555 internal fun listenForLockscreenSettingChanges(scope: CoroutineScope): Job { 556 return scope.launch { 557 secureSettings 558 .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) 559 // query to get initial value 560 .onStart { emit(Unit) } 561 .map { getMediaLockScreenSetting() } 562 .distinctUntilChanged() 563 .flowOn(backgroundDispatcher) 564 .collectLatest { 565 allowMediaPlayerOnLockScreen = it 566 updateHostVisibility() 567 } 568 } 569 } 570 571 @VisibleForTesting 572 internal fun listenForAnyStateToDozingTransition(scope: CoroutineScope): Job { 573 return scope.launch { 574 keyguardTransitionInteractor 575 .transition(Edge.create(to = DOZING)) 576 .filter { it.transitionState == TransitionState.FINISHED } 577 .collect { 578 if (!allowMediaPlayerOnLockScreen) { 579 updateHostVisibility() 580 } 581 } 582 } 583 } 584 585 private fun listenForMediaItemsChanges(scope: CoroutineScope): Job { 586 return scope.launch { 587 mediaCarouselViewModel.mediaItems.collectLatest { 588 val diffUtilCallback = MediaViewModelCallback(controlViewModels, it) 589 val listUpdateCallback = 590 MediaViewModelListUpdateCallback( 591 old = controlViewModels, 592 new = it, 593 onAdded = this@MediaCarouselController::onAdded, 594 onUpdated = this@MediaCarouselController::onUpdated, 595 onRemoved = this@MediaCarouselController::onRemoved, 596 onMoved = this@MediaCarouselController::onMoved, 597 ) 598 DiffUtil.calculateDiff(diffUtilCallback).dispatchUpdatesTo(listUpdateCallback) 599 setNewViewModelsList(it) 600 601 // Update host visibility when media changes. 602 merge( 603 mediaCarouselViewModel.hasAnyMediaOrRecommendations, 604 mediaCarouselViewModel.hasActiveMediaOrRecommendations, 605 ) 606 .collect { updateHostVisibility() } 607 } 608 } 609 } 610 611 private fun onAdded( 612 controlViewModel: MediaControlViewModel, 613 position: Int, 614 configChanged: Boolean = false, 615 ) { 616 val viewController = mediaViewControllerFactory.get() 617 viewController.sizeChangedListener = this::updateCarouselDimensions 618 val lp = 619 LinearLayout.LayoutParams( 620 ViewGroup.LayoutParams.MATCH_PARENT, 621 ViewGroup.LayoutParams.WRAP_CONTENT, 622 ) 623 val viewHolder = MediaViewHolder.create(LayoutInflater.from(context), mediaContent) 624 viewController.widthInSceneContainerPx = widthInSceneContainerPx 625 viewController.heightInSceneContainerPx = heightInSceneContainerPx 626 viewController.attachPlayer(viewHolder) 627 viewController.mediaViewHolder?.player?.layoutParams = lp 628 if (configChanged) { 629 controlViewModel.onMediaConfigChanged() 630 } 631 MediaControlViewBinder.bind( 632 viewHolder, 633 controlViewModel, 634 viewController, 635 falsingManager, 636 backgroundDispatcher, 637 mainDispatcher, 638 ) 639 mediaContent.addView(viewHolder.player, position) 640 controllerById[controlViewModel.instanceId] = viewController 641 viewController.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded) 642 updateViewControllerToState(viewController, noAnimation = true) 643 updatePageIndicator() 644 mediaCarouselScrollHandler.onPlayersChanged() 645 mediaFrame.requiresRemeasuring = true 646 controlViewModel.onAdded(controlViewModel) 647 } 648 649 private fun onUpdated(controlViewModel: MediaControlViewModel, position: Int) { 650 controlViewModel.onUpdated(controlViewModel) 651 updatePageIndicator() 652 mediaCarouselScrollHandler.onPlayersChanged() 653 } 654 655 private fun onRemoved(controlViewModel: MediaControlViewModel) { 656 val id = controlViewModel.instanceId 657 controllerById.remove(id)?.let { 658 mediaCarouselScrollHandler.onPrePlayerRemoved(it.mediaViewHolder!!.player) 659 mediaContent.removeView(it.mediaViewHolder!!.player) 660 it.onDestroy() 661 mediaCarouselScrollHandler.onPlayersChanged() 662 updatePageIndicator() 663 controlViewModel.onRemoved(true) 664 } 665 } 666 667 private fun onMoved(controlViewModel: MediaControlViewModel, from: Int, to: Int) { 668 val id = controlViewModel.instanceId 669 controllerById[id]?.let { 670 mediaContent.removeViewAt(from) 671 mediaContent.addView(it.mediaViewHolder!!.player, to) 672 } 673 updatePageIndicator() 674 mediaCarouselScrollHandler.onPlayersChanged() 675 } 676 677 private fun setNewViewModelsList(viewModels: List<MediaControlViewModel>) { 678 controlViewModels.clear() 679 controlViewModels.addAll(viewModels) 680 681 // Ensure we only show the needed UMOs in media carousel. 682 val viewIds = viewModels.map { controlViewModel -> controlViewModel.instanceId }.toHashSet() 683 controllerById 684 .filter { !viewIds.contains(it.key) } 685 .forEach { 686 mediaCarouselScrollHandler.onPrePlayerRemoved(it.value.mediaViewHolder?.player) 687 mediaContent.removeView(it.value.mediaViewHolder?.player) 688 it.value.onDestroy() 689 mediaCarouselScrollHandler.onPlayersChanged() 690 updatePageIndicator() 691 } 692 } 693 694 private suspend fun getMediaLockScreenSetting(): Boolean { 695 return withContext(backgroundDispatcher) { 696 secureSettings.getBoolForUser( 697 Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 698 true, 699 UserHandle.USER_CURRENT, 700 ) 701 } 702 } 703 704 fun setSceneContainerSize(width: Int, height: Int) { 705 if (width == widthInSceneContainerPx && height == heightInSceneContainerPx) { 706 return 707 } 708 if (width <= 0 || height <= 0) { 709 // reject as invalid 710 return 711 } 712 widthInSceneContainerPx = width 713 heightInSceneContainerPx = height 714 mediaCarouselScrollHandler.playerWidthPlusPadding = 715 width + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) 716 updatePlayers(recreateMedia = true) 717 } 718 719 /** Return true if the carousel should be hidden because device is locked. */ 720 fun isLockedAndHidden(): Boolean { 721 val isOnLockscreen = 722 if (SceneContainerFlag.isEnabled) { 723 !deviceEntryInteractor.isDeviceEntered.value 724 } else { 725 !isOnGone.value || isGoingToDozing.value 726 } 727 return !allowMediaPlayerOnLockScreen && isOnLockscreen 728 } 729 730 private fun reorderAllPlayers( 731 previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?, 732 key: String? = null, 733 ) { 734 mediaContent.removeAllViews() 735 for (mediaPlayer in MediaPlayerData.players()) { 736 mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) } 737 } 738 mediaCarouselScrollHandler.onPlayersChanged() 739 mediaControlChipInteractor.updateMediaControlChipModelLegacy( 740 MediaPlayerData.getFirstActiveMediaData() 741 ) 742 MediaPlayerData.updateVisibleMediaPlayers() 743 if (isRtl && mediaContent.childCount > 0) { 744 // In RTL, Scroll to the first player as it is the rightmost player in media carousel. 745 mediaCarouselScrollHandler.scrollToPlayer(destIndex = 0) 746 } 747 // Check postcondition: mediaContent should have the same number of children as there are 748 // elements in mediaPlayers. 749 if (MediaPlayerData.players().size != mediaContent.childCount) { 750 Log.e( 751 TAG, 752 "Size of players list and number of views in carousel are out of sync. " + 753 "Players size is ${MediaPlayerData.players().size}. " + 754 "View count is ${mediaContent.childCount}.", 755 ) 756 } 757 } 758 759 // Returns true if new player is added 760 private fun addOrUpdatePlayer( 761 key: String, 762 oldKey: String?, 763 data: MediaData, 764 onUiExecutionEnd: Runnable? = null, 765 ): Boolean = 766 traceSection("MediaCarouselController#addOrUpdatePlayer") { 767 MediaPlayerData.moveIfExists(oldKey, key) 768 val existingPlayer = MediaPlayerData.getMediaPlayer(key) 769 val curVisibleMediaKey = 770 MediaPlayerData.visiblePlayerKeys() 771 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) 772 if (mediaControlsUmoInflationInBackground()) { 773 if (existingPlayer == null) { 774 bgExecutor.execute { 775 val mediaViewHolder = createMediaViewHolderInBg() 776 // Add the new player in the main thread. 777 uiExecutor.execute { 778 setupNewPlayer(key, data, curVisibleMediaKey, mediaViewHolder) 779 updatePageIndicator() 780 mediaCarouselScrollHandler.onPlayersChanged() 781 mediaControlChipInteractor.updateMediaControlChipModelLegacy( 782 MediaPlayerData.getFirstActiveMediaData() 783 ) 784 mediaFrame.requiresRemeasuring = true 785 onUiExecutionEnd?.run() 786 } 787 } 788 } else { 789 updatePlayer(key, data, curVisibleMediaKey, existingPlayer) 790 updatePageIndicator() 791 mediaCarouselScrollHandler.onPlayersChanged() 792 mediaControlChipInteractor.updateMediaControlChipModelLegacy( 793 MediaPlayerData.getFirstActiveMediaData() 794 ) 795 mediaFrame.requiresRemeasuring = true 796 onUiExecutionEnd?.run() 797 } 798 } else { 799 if (existingPlayer == null) { 800 val mediaViewHolder = 801 MediaViewHolder.create(LayoutInflater.from(context), mediaContent) 802 setupNewPlayer(key, data, curVisibleMediaKey, mediaViewHolder) 803 } else { 804 updatePlayer(key, data, curVisibleMediaKey, existingPlayer) 805 } 806 updatePageIndicator() 807 mediaCarouselScrollHandler.onPlayersChanged() 808 mediaControlChipInteractor.updateMediaControlChipModelLegacy( 809 MediaPlayerData.getFirstActiveMediaData() 810 ) 811 mediaFrame.requiresRemeasuring = true 812 onUiExecutionEnd?.run() 813 } 814 return existingPlayer == null 815 } 816 817 private fun updatePlayer( 818 key: String, 819 data: MediaData, 820 curVisibleMediaKey: MediaPlayerData.MediaSortKey?, 821 existingPlayer: MediaControlPanel, 822 ) { 823 existingPlayer.bindPlayer(data, key) 824 MediaPlayerData.addMediaPlayer(key, data, existingPlayer, systemClock, debugLogger) 825 if (isReorderingAllowed) { 826 reorderAllPlayers(curVisibleMediaKey, key) 827 } else { 828 needsReordering = true 829 } 830 } 831 832 private fun setupNewPlayer( 833 key: String, 834 data: MediaData, 835 curVisibleMediaKey: MediaPlayerData.MediaSortKey?, 836 mediaViewHolder: MediaViewHolder, 837 ) { 838 val newPlayer = mediaControlPanelFactory.get() 839 newPlayer.attachPlayer(mediaViewHolder) 840 newPlayer.mediaViewController.sizeChangedListener = 841 this@MediaCarouselController::updateCarouselDimensions 842 val lp = 843 LinearLayout.LayoutParams( 844 ViewGroup.LayoutParams.MATCH_PARENT, 845 ViewGroup.LayoutParams.WRAP_CONTENT, 846 ) 847 newPlayer.mediaViewHolder?.player?.setLayoutParams(lp) 848 newPlayer.bindPlayer(data, key) 849 newPlayer.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded) 850 MediaPlayerData.addMediaPlayer(key, data, newPlayer, systemClock, debugLogger) 851 updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true) 852 if (data.active) { 853 reorderAllPlayers(curVisibleMediaKey, key) 854 } else { 855 needsReordering = true 856 } 857 } 858 859 @WorkerThread 860 private fun createMediaViewHolderInBg(): MediaViewHolder { 861 return MediaViewHolder.create(LayoutInflater.from(context), mediaContent) 862 } 863 864 fun removePlayer( 865 key: String, 866 dismissMediaData: Boolean = true, 867 userInitiated: Boolean = false, 868 ): MediaControlPanel? { 869 val removed = MediaPlayerData.removeMediaPlayer(key, dismissMediaData) 870 return removed?.apply { 871 mediaCarouselScrollHandler.onPrePlayerRemoved(removed.mediaViewHolder?.player) 872 mediaContent.removeView(removed.mediaViewHolder?.player) 873 removed.onDestroy() 874 mediaCarouselScrollHandler.onPlayersChanged() 875 mediaControlChipInteractor.updateMediaControlChipModelLegacy( 876 MediaPlayerData.getFirstActiveMediaData() 877 ) 878 updatePageIndicator() 879 880 if (dismissMediaData) { 881 // Inform the media manager of a potentially late dismissal 882 mediaManager.dismissMediaData(key, delay = 0L, userInitiated = userInitiated) 883 } 884 } 885 } 886 887 private fun updatePlayers(recreateMedia: Boolean) { 888 if (SceneContainerFlag.isEnabled) { 889 updateMediaPlayers(recreateMedia) 890 return 891 } 892 pageIndicator.tintList = 893 ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) 894 val previousVisibleKey = 895 MediaPlayerData.visiblePlayerKeys() 896 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) 897 val onUiExecutionEnd = Runnable { 898 if (recreateMedia) { 899 reorderAllPlayers(previousVisibleKey) 900 } 901 } 902 903 val mediaDataList = MediaPlayerData.mediaData() 904 // Do not loop through the original list of media data because the re-addition of media data 905 // is being executed in background thread. 906 mediaDataList.forEach { (key, data) -> 907 if (recreateMedia) { 908 removePlayer(key, dismissMediaData = false) 909 } 910 addOrUpdatePlayer( 911 key = key, 912 oldKey = null, 913 data = data, 914 onUiExecutionEnd = onUiExecutionEnd, 915 ) 916 } 917 } 918 919 private fun updateMediaPlayers(recreateMedia: Boolean) { 920 pageIndicator.tintList = 921 ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) 922 if (recreateMedia) { 923 mediaContent.removeAllViews() 924 controlViewModels.forEachIndexed { index, viewModel -> 925 controllerById[viewModel.instanceId]?.onDestroy() 926 onAdded(viewModel, index, configChanged = true) 927 } 928 } 929 } 930 931 private fun updatePageIndicator() { 932 val numPages = mediaContent.getChildCount() 933 pageIndicator.setNumPages(numPages) 934 if (numPages == 1) { 935 pageIndicator.setLocation(0f) 936 } 937 updatePageIndicatorAlpha() 938 } 939 940 /** 941 * Set a new interpolated state for all players. This is a state that is usually controlled by a 942 * finger movement where the user drags from one state to the next. 943 * 944 * @param startLocation the start location of our state or -1 if this is directly set 945 * @param endLocation the ending location of our state. 946 * @param progress the progress of the transition between startLocation and endlocation. If 947 * 948 * ``` 949 * this is not a guided transformation, this will be 1.0f 950 * @param immediately 951 * ``` 952 * 953 * should this state be applied immediately, canceling all animations? 954 */ 955 fun setCurrentState( 956 @MediaLocation startLocation: Int, 957 @MediaLocation endLocation: Int, 958 progress: Float, 959 immediately: Boolean, 960 ) { 961 if ( 962 startLocation != currentStartLocation || 963 endLocation != currentEndLocation || 964 progress != currentTransitionProgress || 965 immediately 966 ) { 967 currentStartLocation = startLocation 968 currentEndLocation = endLocation 969 currentTransitionProgress = progress 970 if (!SceneContainerFlag.isEnabled) { 971 for (mediaPlayer in MediaPlayerData.players()) { 972 updateViewControllerToState(mediaPlayer.mediaViewController, immediately) 973 } 974 } else { 975 controllerById.values.forEach { updateViewControllerToState(it, immediately) } 976 } 977 maybeResetSettingsCog() 978 updatePageIndicatorAlpha() 979 } 980 } 981 982 @VisibleForTesting 983 fun updatePageIndicatorAlpha() { 984 val hostStates = mediaHostStatesManager.mediaHostStates 985 val endIsVisible = hostStates[currentEndLocation]?.visible ?: false 986 val startIsVisible = hostStates[currentStartLocation]?.visible ?: false 987 val startAlpha = if (startIsVisible) 1.0f else 0.0f 988 // when squishing in split shade, only use endState, which keeps changing 989 // to provide squishFraction 990 val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F 991 val endAlpha = 992 (if (endIsVisible) 1.0f else 0.0f) * 993 calculateAlpha( 994 squishFraction, 995 (pageIndicator.translationY + pageIndicator.height) / 996 mediaCarousel.measuredHeight, 997 1F, 998 ) 999 var alpha = 1.0f 1000 if (!endIsVisible || !startIsVisible) { 1001 var progress = currentTransitionProgress 1002 if (!endIsVisible) { 1003 progress = 1.0f - progress 1004 } 1005 // Let's fade in quickly at the end where the view is visible 1006 progress = 1007 MathUtils.constrain(MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), 0.0f, 1.0f) 1008 alpha = MathUtils.lerp(startAlpha, endAlpha, progress) 1009 } 1010 pageIndicator.alpha = alpha 1011 } 1012 1013 private fun updatePageIndicatorLocation() { 1014 // Update the location of the page indicator, carousel clipping 1015 val translationX = 1016 if (isRtl) { 1017 (pageIndicator.width - currentCarouselWidth) / 2.0f 1018 } else { 1019 (currentCarouselWidth - pageIndicator.width) / 2.0f 1020 } 1021 pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation 1022 val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams 1023 pageIndicator.translationY = 1024 (mediaCarousel.measuredHeight - pageIndicator.height - layoutParams.bottomMargin) 1025 .toFloat() 1026 } 1027 1028 /** Update listening to seekbar. */ 1029 private fun updateSeekbarListening(visibleToUser: Boolean) { 1030 if (!SceneContainerFlag.isEnabled) { 1031 for (player in MediaPlayerData.players()) { 1032 player.setListening(visibleToUser && currentlyExpanded) 1033 } 1034 } else { 1035 controllerById.values.forEach { it.setListening(visibleToUser && currentlyExpanded) } 1036 } 1037 } 1038 1039 /** Update the dimension of this carousel. */ 1040 private fun updateCarouselDimensions() { 1041 var width = 0 1042 var height = 0 1043 if (!SceneContainerFlag.isEnabled) { 1044 for (mediaPlayer in MediaPlayerData.players()) { 1045 val controller = mediaPlayer.mediaViewController 1046 // When transitioning the view to gone, the view gets smaller, but the translation 1047 // Doesn't, let's add the translation 1048 width = Math.max(width, controller.currentWidth + controller.translationX.toInt()) 1049 height = 1050 Math.max(height, controller.currentHeight + controller.translationY.toInt()) 1051 } 1052 } else { 1053 controllerById.values.forEach { 1054 // When transitioning the view to gone, the view gets smaller, but the translation 1055 // Doesn't, let's add the translation 1056 width = Math.max(width, it.currentWidth + it.translationX.toInt()) 1057 height = Math.max(height, it.currentHeight + it.translationY.toInt()) 1058 } 1059 } 1060 if (width != currentCarouselWidth || height != currentCarouselHeight) { 1061 currentCarouselWidth = width 1062 currentCarouselHeight = height 1063 mediaCarouselScrollHandler.setCarouselBounds( 1064 currentCarouselWidth, 1065 currentCarouselHeight, 1066 ) 1067 updatePageIndicatorLocation() 1068 updatePageIndicatorAlpha() 1069 } 1070 } 1071 1072 private fun maybeResetSettingsCog() { 1073 val hostStates = mediaHostStatesManager.mediaHostStates 1074 val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia ?: true 1075 val startShowsActive = 1076 hostStates[currentStartLocation]?.showsOnlyActiveMedia ?: endShowsActive 1077 val startDisableScrolling = hostStates[currentStartLocation]?.disableScrolling ?: false 1078 val endDisableScrolling = hostStates[currentEndLocation]?.disableScrolling ?: false 1079 1080 if ( 1081 currentlyShowingOnlyActive != endShowsActive || 1082 currentlyDisableScrolling != endDisableScrolling || 1083 ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && 1084 (startShowsActive != endShowsActive || 1085 startDisableScrolling != endDisableScrolling)) 1086 ) { 1087 // Whenever we're transitioning from between differing states or the endstate differs 1088 // we reset the translation 1089 currentlyShowingOnlyActive = endShowsActive 1090 currentlyDisableScrolling = endDisableScrolling 1091 mediaCarouselScrollHandler.resetTranslation(animate = true) 1092 mediaCarouselScrollHandler.scrollingDisabled = currentlyDisableScrolling 1093 } 1094 } 1095 1096 private fun updateViewControllerToState( 1097 viewController: MediaViewController, 1098 noAnimation: Boolean, 1099 ) { 1100 viewController.setCurrentState( 1101 startLocation = currentStartLocation, 1102 endLocation = currentEndLocation, 1103 transitionProgress = currentTransitionProgress, 1104 applyImmediately = noAnimation, 1105 ) 1106 } 1107 1108 /** 1109 * The desired location of this view has changed. We should remeasure the view to match the new 1110 * bounds and kick off bounds animations if necessary. If an animation is happening, an 1111 * animation is kicked of externally, which sets a new current state until we reach the 1112 * targetState. 1113 * 1114 * @param desiredLocation the location we're going to 1115 * @param desiredHostState the target state we're transitioning to 1116 * @param animate should this be animated 1117 */ 1118 fun onDesiredLocationChanged( 1119 desiredLocation: Int, 1120 desiredHostState: MediaHostState?, 1121 animate: Boolean, 1122 duration: Long = 200, 1123 startDelay: Long = 0, 1124 ) = 1125 traceSection("MediaCarouselController#onDesiredLocationChanged") { 1126 desiredHostState?.let { 1127 if (this.desiredLocation != desiredLocation) { 1128 // Only log an event when location changes 1129 bgExecutor.execute { logger.logCarouselPosition(desiredLocation) } 1130 } 1131 1132 // This is a hosting view, let's remeasure our players 1133 val prevLocation = this.desiredLocation 1134 this.desiredLocation = desiredLocation 1135 this.desiredHostState = it 1136 currentlyExpanded = it.expansion > 0 1137 1138 // Set color of the settings button to material "on primary" color when media is on 1139 // communal for aesthetic and accessibility purposes since the background of 1140 // Glanceable Hub is a dynamic color. 1141 if (desiredLocation == MediaHierarchyManager.LOCATION_COMMUNAL_HUB) { 1142 settingsButton.setColorFilter( 1143 context.getColor(com.android.internal.R.color.materialColorOnPrimary) 1144 ) 1145 } else { 1146 settingsButton.setColorFilter(context.getColor(R.color.notification_gear_color)) 1147 } 1148 1149 val shouldCloseGuts = 1150 !currentlyExpanded && 1151 !mediaManager.hasActiveMediaOrRecommendation() && 1152 desiredHostState.showsOnlyActiveMedia 1153 1154 if (!SceneContainerFlag.isEnabled) { 1155 for (mediaPlayer in MediaPlayerData.players()) { 1156 if (animate) { 1157 mediaPlayer.mediaViewController.animatePendingStateChange( 1158 duration = duration, 1159 delay = startDelay, 1160 ) 1161 } 1162 if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) { 1163 mediaPlayer.closeGuts(!animate) 1164 } 1165 1166 mediaPlayer.mediaViewController.onLocationPreChange( 1167 mediaPlayer.mediaViewHolder, 1168 desiredLocation, 1169 prevLocation, 1170 ) 1171 } 1172 } else { 1173 controllerById.values.forEach { controller -> 1174 if (animate) { 1175 controller.animatePendingStateChange(duration, startDelay) 1176 } 1177 if (shouldCloseGuts && controller.isGutsVisible) { 1178 controller.closeGuts(!animate) 1179 } 1180 1181 controller.onLocationPreChange( 1182 controller.mediaViewHolder, 1183 desiredLocation, 1184 prevLocation, 1185 ) 1186 } 1187 } 1188 mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia 1189 mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded 1190 val nowVisible = it.visible 1191 if (nowVisible != playersVisible) { 1192 playersVisible = nowVisible 1193 if (nowVisible) { 1194 mediaCarouselScrollHandler.resetTranslation() 1195 } 1196 } 1197 updateCarouselSize() 1198 } 1199 } 1200 1201 fun closeGuts(immediate: Boolean = true) { 1202 if (!SceneContainerFlag.isEnabled) { 1203 MediaPlayerData.players().forEach { it.closeGuts(immediate) } 1204 } else { 1205 controllerById.values.forEach { it.closeGuts(immediate) } 1206 } 1207 } 1208 1209 /** Update the size of the carousel, remeasuring it if necessary. */ 1210 private fun updateCarouselSize() { 1211 val width = desiredHostState?.measurementInput?.width ?: 0 1212 val height = desiredHostState?.measurementInput?.height ?: 0 1213 if ( 1214 width != carouselMeasureWidth && width != 0 || 1215 height != carouselMeasureHeight && height != 0 1216 ) { 1217 carouselMeasureWidth = width 1218 carouselMeasureHeight = height 1219 val playerWidthPlusPadding = 1220 carouselMeasureWidth + 1221 context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) 1222 // Let's remeasure the carousel 1223 val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 1224 val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 1225 mediaCarousel.measure(widthSpec, heightSpec) 1226 mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) 1227 // Update the padding after layout; view widths are used in RTL to calculate scrollX 1228 mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding 1229 } 1230 } 1231 1232 @VisibleForTesting 1233 fun onSwipeToDismiss() { 1234 if (SceneContainerFlag.isEnabled) { 1235 mediaCarouselViewModel.onSwipeToDismiss() 1236 return 1237 } 1238 MediaPlayerData.isSwipedAway = true 1239 logger.logSwipeDismiss() 1240 mediaManager.onSwipeToDismiss() 1241 } 1242 1243 fun getCurrentVisibleMediaContentIntent(): PendingIntent? { 1244 return MediaPlayerData.playerKeys() 1245 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) 1246 ?.data 1247 ?.clickIntent 1248 } 1249 1250 override fun dump(pw: PrintWriter, args: Array<out String>) { 1251 pw.apply { 1252 println("keysNeedRemoval: $keysNeedRemoval") 1253 println("dataKeys: ${MediaPlayerData.dataKeys()}") 1254 println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}") 1255 println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}") 1256 println("controlViewModels: $controlViewModels") 1257 println("current size: $currentCarouselWidth x $currentCarouselHeight") 1258 println("location: $desiredLocation") 1259 println( 1260 "state: ${desiredHostState?.expansion}, " + 1261 "only active ${desiredHostState?.showsOnlyActiveMedia}, " + 1262 "visible ${desiredHostState?.visible}" 1263 ) 1264 println("isSwipedAway: ${MediaPlayerData.isSwipedAway}") 1265 println("allowMediaPlayerOnLockScreen: $allowMediaPlayerOnLockScreen") 1266 } 1267 } 1268 } 1269 1270 @VisibleForTesting 1271 internal object MediaPlayerData { 1272 private val EMPTY = 1273 MediaData( 1274 userId = -1, 1275 initialized = false, 1276 app = null, 1277 appIcon = null, 1278 artist = null, 1279 song = null, 1280 artwork = null, 1281 actions = emptyList(), 1282 actionsToShowInCompact = emptyList(), 1283 packageName = "INVALID", 1284 token = null, 1285 clickIntent = null, 1286 device = null, 1287 active = true, 1288 resumeAction = null, 1289 instanceId = InstanceId.fakeInstanceId(-1), 1290 appUid = -1, 1291 ) 1292 1293 data class MediaSortKey(val data: MediaData, val key: String, val updateTime: Long = 0) 1294 1295 private val comparator = <lambda>null1296 compareByDescending<MediaSortKey> { 1297 it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL 1298 } <lambda>null1299 .thenByDescending { 1300 it.data.isPlaying == true && 1301 it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL 1302 } <lambda>null1303 .thenByDescending { it.data.active } <lambda>null1304 .thenByDescending { !it.data.resumption } <lambda>null1305 .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE } <lambda>null1306 .thenByDescending { it.data.lastActive } <lambda>null1307 .thenByDescending { it.updateTime } <lambda>null1308 .thenByDescending { it.data.notificationKey } 1309 1310 private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) 1311 private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() 1312 1313 // A map that tracks order of visible media players before they get reordered. 1314 private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>() 1315 1316 // Whether the user swiped away the carousel since its last update 1317 internal var isSwipedAway: Boolean = false 1318 addMediaPlayernull1319 fun addMediaPlayer( 1320 key: String, 1321 data: MediaData, 1322 player: MediaControlPanel, 1323 clock: SystemClock, 1324 debugLogger: MediaCarouselControllerLogger? = null, 1325 ) { 1326 val removedPlayer = removeMediaPlayer(key) 1327 if (removedPlayer != null && removedPlayer != player) { 1328 debugLogger?.logPotentialMemoryLeak(key) 1329 removedPlayer.onDestroy() 1330 } 1331 val sortKey = MediaSortKey(data, key, clock.currentTimeMillis()) 1332 mediaData.put(key, sortKey) 1333 mediaPlayers.put(sortKey, player) 1334 visibleMediaPlayers.put(key, sortKey) 1335 } 1336 moveIfExistsnull1337 fun moveIfExists( 1338 oldKey: String?, 1339 newKey: String, 1340 debugLogger: MediaCarouselControllerLogger? = null, 1341 ) { 1342 if (oldKey == null || oldKey == newKey) { 1343 return 1344 } 1345 1346 mediaData.remove(oldKey)?.let { 1347 // MediaPlayer should not be visible 1348 // no need to set isDismissed flag. 1349 val removedPlayer = removeMediaPlayer(newKey) 1350 removedPlayer?.run { 1351 debugLogger?.logPotentialMemoryLeak(newKey) 1352 onDestroy() 1353 } 1354 mediaData.put(newKey, it) 1355 } 1356 } 1357 getMediaControlPanelnull1358 fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? { 1359 return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex)) 1360 } 1361 getMediaPlayernull1362 fun getMediaPlayer(key: String): MediaControlPanel? { 1363 return mediaData.get(key)?.let { mediaPlayers.get(it) } 1364 } 1365 getMediaPlayerIndexnull1366 fun getMediaPlayerIndex(key: String): Int { 1367 val sortKey = mediaData.get(key) 1368 mediaPlayers.entries.forEachIndexed { index, e -> 1369 if (e.key == sortKey) { 1370 return index 1371 } 1372 } 1373 return -1 1374 } 1375 1376 /** 1377 * Removes media player given the key. 1378 * 1379 * @param isDismissed determines whether the media player is removed from the carousel. 1380 */ removeMediaPlayernull1381 fun removeMediaPlayer(key: String, isDismissed: Boolean = false) = 1382 mediaData.remove(key)?.let { 1383 if (isDismissed) { 1384 visibleMediaPlayers.remove(key) 1385 } 1386 mediaPlayers.remove(it) 1387 } 1388 mediaDatanull1389 fun mediaData() = mediaData.entries.map { e -> Pair(e.key, e.value.data) } 1390 dataKeysnull1391 fun dataKeys() = mediaData.keys 1392 1393 fun players() = mediaPlayers.values 1394 1395 fun playerKeys() = mediaPlayers.keys 1396 1397 fun visiblePlayerKeys() = visibleMediaPlayers.values 1398 1399 /** Returns the [MediaData] associated with the first mediaPlayer in the mediaCarousel. */ 1400 fun getFirstActiveMediaData(): MediaData? { 1401 // TODO simplify ..?? 1402 mediaPlayers.entries.forEach { entry -> 1403 if (entry.key.data.active) { 1404 return entry.key.data 1405 } 1406 } 1407 return null 1408 } 1409 1410 /** Returns the index of the first non-timeout media. */ firstActiveMediaIndexnull1411 fun firstActiveMediaIndex(): Int { 1412 // TODO simplify? 1413 mediaPlayers.entries.forEachIndexed { index, e -> 1414 if (e.key.data.active) { 1415 return index 1416 } 1417 } 1418 return -1 1419 } 1420 1421 @VisibleForTesting clearnull1422 fun clear() { 1423 mediaData.clear() 1424 mediaPlayers.clear() 1425 visibleMediaPlayers.clear() 1426 } 1427 1428 /** 1429 * This method is called when media players are reordered. To make sure we have the new version 1430 * of the order of media players visible to user. 1431 */ updateVisibleMediaPlayersnull1432 fun updateVisibleMediaPlayers() { 1433 visibleMediaPlayers.clear() 1434 playerKeys().forEach { visibleMediaPlayers.put(it.key, it) } 1435 } 1436 } 1437