• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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