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.LinearLayout
36 import androidx.annotation.VisibleForTesting
37 import androidx.lifecycle.Lifecycle
38 import androidx.lifecycle.repeatOnLifecycle
39 import androidx.recyclerview.widget.DiffUtil
40 import com.android.app.tracing.coroutines.launchTraced as launch
41 import com.android.app.tracing.traceSection
42 import com.android.internal.logging.InstanceId
43 import com.android.keyguard.KeyguardUpdateMonitor
44 import com.android.keyguard.KeyguardUpdateMonitorCallback
45 import com.android.systemui.Dumpable
46 import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
47 import com.android.systemui.dagger.SysUISingleton
48 import com.android.systemui.dagger.qualifiers.Application
49 import com.android.systemui.dagger.qualifiers.Background
50 import com.android.systemui.dagger.qualifiers.Main
51 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
52 import com.android.systemui.dump.DumpManager
53 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
54 import com.android.systemui.keyguard.shared.model.Edge
55 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
56 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
57 import com.android.systemui.keyguard.shared.model.TransitionState
58 import com.android.systemui.lifecycle.repeatWhenAttached
59 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
60 import com.android.systemui.media.controls.shared.model.MediaData
61 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
62 import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder
63 import com.android.systemui.media.controls.ui.binder.MediaRecommendationsViewBinder
64 import com.android.systemui.media.controls.ui.controller.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
65 import com.android.systemui.media.controls.ui.util.MediaViewModelCallback
66 import com.android.systemui.media.controls.ui.util.MediaViewModelListUpdateCallback
67 import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler
68 import com.android.systemui.media.controls.ui.view.MediaHostState
69 import com.android.systemui.media.controls.ui.view.MediaScrollView
70 import com.android.systemui.media.controls.ui.view.MediaViewHolder
71 import com.android.systemui.media.controls.ui.view.RecommendationViewHolder
72 import com.android.systemui.media.controls.ui.viewmodel.MediaCarouselViewModel
73 import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel
74 import com.android.systemui.media.controls.util.MediaFlags
75 import com.android.systemui.media.controls.util.MediaUiEventLogger
76 import com.android.systemui.media.controls.util.SmallHash
77 import com.android.systemui.plugins.ActivityStarter
78 import com.android.systemui.plugins.FalsingManager
79 import com.android.systemui.qs.PageIndicator
80 import com.android.systemui.res.R
81 import com.android.systemui.scene.shared.flag.SceneContainerFlag
82 import com.android.systemui.scene.shared.model.Scenes
83 import com.android.systemui.shared.system.SysUiStatsLog
84 import com.android.systemui.shared.system.SysUiStatsLog.SMARTSPACE_CARD_REPORTED
85 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD
86 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY as SSPACE_CARD_REPORTED__DREAM_OVERLAY
87 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN as SSPACE_CARD_REPORTED__LOCKSCREEN
88 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE
89 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
90 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
91 import com.android.systemui.statusbar.policy.ConfigurationController
92 import com.android.systemui.util.Utils
93 import com.android.systemui.util.animation.UniqueObjectHostView
94 import com.android.systemui.util.animation.requiresRemeasuring
95 import com.android.systemui.util.concurrency.DelayableExecutor
96 import com.android.systemui.util.settings.GlobalSettings
97 import com.android.systemui.util.settings.SecureSettings
98 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
99 import com.android.systemui.util.time.SystemClock
100 import java.io.PrintWriter
101 import java.util.Locale
102 import java.util.TreeMap
103 import java.util.concurrent.Executor
104 import javax.inject.Inject
105 import javax.inject.Provider
106 import kotlinx.coroutines.CoroutineDispatcher
107 import kotlinx.coroutines.CoroutineScope
108 import kotlinx.coroutines.ExperimentalCoroutinesApi
109 import kotlinx.coroutines.Job
110 import kotlinx.coroutines.flow.SharingStarted
111 import kotlinx.coroutines.flow.collectLatest
112 import kotlinx.coroutines.flow.distinctUntilChanged
113 import kotlinx.coroutines.flow.filter
114 import kotlinx.coroutines.flow.flowOn
115 import kotlinx.coroutines.flow.map
116 import kotlinx.coroutines.flow.merge
117 import kotlinx.coroutines.flow.onStart
118 import kotlinx.coroutines.flow.stateIn
119 import kotlinx.coroutines.withContext
120 
121 private const val TAG = "MediaCarouselController"
122 private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
123 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
124 
125 /**
126  * Class that is responsible for keeping the view carousel up to date. This also handles changes in
127  * state and applies them to the media carousel like the expansion.
128  */
129 @OptIn(ExperimentalCoroutinesApi::class)
130 @SysUISingleton
131 class MediaCarouselController
132 @Inject
133 constructor(
134     @Application applicationScope: CoroutineScope,
135     private val context: Context,
136     private val mediaControlPanelFactory: Provider<MediaControlPanel>,
137     private val visualStabilityProvider: VisualStabilityProvider,
138     private val mediaHostStatesManager: MediaHostStatesManager,
139     private val activityStarter: ActivityStarter,
140     private val systemClock: SystemClock,
141     @Main private val mainDispatcher: CoroutineDispatcher,
142     @Main private val uiExecutor: DelayableExecutor,
143     @Background private val bgExecutor: Executor,
144     @Background private val backgroundDispatcher: CoroutineDispatcher,
145     private val mediaManager: MediaDataManager,
146     configurationController: ConfigurationController,
147     private val falsingManager: FalsingManager,
148     dumpManager: DumpManager,
149     private val logger: MediaUiEventLogger,
150     private val debugLogger: MediaCarouselControllerLogger,
151     private val mediaFlags: MediaFlags,
152     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
153     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
154     private val globalSettings: GlobalSettings,
155     private val secureSettings: SecureSettings,
156     private val mediaCarouselViewModel: MediaCarouselViewModel,
157     private val mediaViewControllerFactory: Provider<MediaViewController>,
158     private val deviceEntryInteractor: DeviceEntryInteractor,
159 ) : Dumpable {
160     /** The current width of the carousel */
161     var currentCarouselWidth: Int = 0
162         private set
163 
164     /** The current height of the carousel */
165     private var currentCarouselHeight: Int = 0
166 
167     /** Are we currently showing only active players */
168     private var currentlyShowingOnlyActive: Boolean = false
169 
170     /** Is the player currently visible (at the end of the transformation */
171     private var playersVisible: Boolean = false
172 
173     /** Are we currently disabling pagination only allowing one media session to show */
174     private var currentlyDisablePagination: Boolean = false
175 
176     /**
177      * The desired location where we'll be at the end of the transformation. Usually this matches
178      * the end location, except when we're still waiting on a state update call.
179      */
180     @MediaLocation private var desiredLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN
181 
182     /**
183      * The ending location of the view where it ends when all animations and transitions have
184      * finished
185      */
186     @MediaLocation
187     @VisibleForTesting
188     var currentEndLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN
189 
190     /**
191      * The ending location of the view where it ends when all animations and transitions have
192      * finished
193      */
194     @MediaLocation private var currentStartLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN
195 
196     /** The progress of the transition or 1.0 if there is no transition happening */
197     private var currentTransitionProgress: Float = 1.0f
198 
199     /** The measured width of the carousel */
200     private var carouselMeasureWidth: Int = 0
201 
202     /** The measured height of the carousel */
203     private var carouselMeasureHeight: Int = 0
204     private var desiredHostState: MediaHostState? = null
205     @VisibleForTesting var mediaCarousel: MediaScrollView
206     val mediaCarouselScrollHandler: MediaCarouselScrollHandler
207     val mediaFrame: ViewGroup
208 
209     @VisibleForTesting
210     lateinit var settingsButton: View
211         private set
212 
213     private val mediaContent: ViewGroup
214     @VisibleForTesting var pageIndicator: PageIndicator
215     private var needsReordering: Boolean = false
216     private var isUserInitiatedRemovalQueued: Boolean = false
217     private var keysNeedRemoval = mutableSetOf<String>()
218     var shouldScrollToKey: Boolean = false
219     private var isRtl: Boolean = false
220         set(value) {
221             if (value != field) {
222                 field = value
223                 mediaFrame.layoutDirection =
224                     if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
225                 mediaCarouselScrollHandler.scrollToStart()
226             }
227         }
228 
229     private var carouselLocale: Locale? = null
230 
231     private val animationScaleObserver: ContentObserver =
232         object : ContentObserver(uiExecutor, 0) {
233             override fun onChange(selfChange: Boolean) {
234                 if (!SceneContainerFlag.isEnabled) {
235                     MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() }
236                 } else {
237                     controllerById.values.forEach { it.updateAnimatorDurationScale() }
238                 }
239             }
240         }
241 
242     private var allowMediaPlayerOnLockScreen = false
243 
244     /** Whether the media card currently has the "expanded" layout */
245     @VisibleForTesting
246     var currentlyExpanded = true
247         set(value) {
248             if (field != value) {
249                 field = value
250                 updateSeekbarListening(mediaCarouselScrollHandler.visibleToUser)
251             }
252         }
253 
254     companion object {
255         val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F)
256 
257         fun calculateAlpha(
258             squishinessFraction: Float,
259             startPosition: Float,
260             endPosition: Float,
261         ): Float {
262             val transformFraction =
263                 MathUtils.constrain(
264                     (squishinessFraction - startPosition) / (endPosition - startPosition),
265                     0F,
266                     1F,
267                 )
268             return TRANSFORM_BEZIER.getInterpolation(transformFraction)
269         }
270     }
271 
272     private val configListener =
273         object : ConfigurationController.ConfigurationListener {
274 
275             override fun onDensityOrFontScaleChanged() {
276                 // System font changes should only happen when UMO is offscreen or a flicker may
277                 // occur
278                 updatePlayers(recreateMedia = true)
279                 inflateSettingsButton()
280             }
281 
282             override fun onThemeChanged() {
283                 updatePlayers(recreateMedia = false)
284                 inflateSettingsButton()
285             }
286 
287             override fun onConfigChanged(newConfig: Configuration?) {
288                 if (newConfig == null) return
289                 isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
290             }
291 
292             override fun onUiModeChanged() {
293                 updatePlayers(recreateMedia = false)
294                 inflateSettingsButton()
295             }
296 
297             override fun onLocaleListChanged() {
298                 // Update players only if system primary language changes.
299                 if (carouselLocale != context.resources.configuration.locales.get(0)) {
300                     carouselLocale = context.resources.configuration.locales.get(0)
301                     updatePlayers(recreateMedia = true)
302                     inflateSettingsButton()
303                 }
304             }
305         }
306 
307     private val keyguardUpdateMonitorCallback =
308         object : KeyguardUpdateMonitorCallback() {
309             override fun onStrongAuthStateChanged(userId: Int) {
310                 if (keyguardUpdateMonitor.isUserInLockdown(userId)) {
311                     debugLogger.logCarouselHidden()
312                     hideMediaCarousel()
313                 } else if (keyguardUpdateMonitor.isUserUnlocked(userId)) {
314                     debugLogger.logCarouselVisible()
315                     showMediaCarousel()
316                 }
317             }
318         }
319 
320     /**
321      * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility.
322      * It will be called when the container is out of view.
323      */
324     lateinit var updateUserVisibility: () -> Unit
325     var updateHostVisibility: () -> Unit = {}
326         set(value) {
327             field = value
328             mediaCarouselViewModel.updateHostVisibility = value
329         }
330 
331     private val isReorderingAllowed: Boolean
332         get() = visualStabilityProvider.isReorderingAllowed
333 
334     /** Size provided by the scene framework container */
335     private var widthInSceneContainerPx = 0
336     private var heightInSceneContainerPx = 0
337 
338     private val controllerById = mutableMapOf<String, MediaViewController>()
339     private val commonViewModels = mutableListOf<MediaCommonViewModel>()
340 
341     private val isOnGone =
342         keyguardTransitionInteractor
343             .isFinishedIn(Scenes.Gone, GONE)
344             .stateIn(applicationScope, SharingStarted.Eagerly, true)
345 
346     init {
347         dumpManager.registerDumpable(TAG, this)
348         mediaFrame = inflateMediaCarousel()
349         mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
350         pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
351         mediaCarouselScrollHandler =
352             MediaCarouselScrollHandler(
353                 mediaCarousel,
354                 pageIndicator,
355                 uiExecutor,
356                 this::onSwipeToDismiss,
357                 this::updatePageIndicatorLocation,
358                 this::updateSeekbarListening,
359                 this::closeGuts,
360                 falsingManager,
361                 this::logSmartspaceImpression,
362                 logger,
363             )
364         carouselLocale = context.resources.configuration.locales.get(0)
365         isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
366         inflateSettingsButton()
367         mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
368         configurationController.addCallback(configListener)
369         if (!SceneContainerFlag.isEnabled) {
370             setUpListeners()
371         } else {
372             val visualStabilityCallback = OnReorderingAllowedListener {
373                 mediaCarouselViewModel.onReorderingAllowed()
374 
375                 // Update user visibility so that no extra impression will be logged when
376                 // activeMediaIndex resets to 0
377                 if (this::updateUserVisibility.isInitialized) {
378                     updateUserVisibility()
379                 }
380 
381                 // Let's reset our scroll position
382                 mediaCarouselScrollHandler.scrollToStart()
383             }
384             visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback)
385         }
386         mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
387             // The pageIndicator is not laid out yet when we get the current state update,
388             // Lets make sure we have the right dimensions
389             updatePageIndicatorLocation()
390         }
391         mediaHostStatesManager.addCallback(
392             object : MediaHostStatesManager.Callback {
393                 override fun onHostStateChanged(
394                     @MediaLocation location: Int,
395                     mediaHostState: MediaHostState,
396                 ) {
397                     updateUserVisibility()
398                     if (location == desiredLocation) {
399                         onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
400                     }
401                 }
402             }
403         )
404         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
405         mediaCarousel.repeatWhenAttached {
406             repeatOnLifecycle(Lifecycle.State.STARTED) {
407                 listenForAnyStateToGoneKeyguardTransition(this)
408                 listenForAnyStateToLockscreenTransition(this)
409 
410                 if (!SceneContainerFlag.isEnabled) return@repeatOnLifecycle
411                 listenForMediaItemsChanges(this)
412             }
413         }
414         listenForLockscreenSettingChanges(applicationScope)
415 
416         // Notifies all active players about animation scale changes.
417         bgExecutor.execute {
418             globalSettings.registerContentObserverSync(
419                 Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
420                 animationScaleObserver,
421             )
422         }
423     }
424 
425     private fun setUpListeners() {
426         val visualStabilityCallback = OnReorderingAllowedListener {
427             if (needsReordering) {
428                 needsReordering = false
429                 reorderAllPlayers(previousVisiblePlayerKey = null)
430             }
431 
432             keysNeedRemoval.forEach {
433                 removePlayer(it, userInitiated = isUserInitiatedRemovalQueued)
434             }
435             if (keysNeedRemoval.size > 0) {
436                 // Carousel visibility may need to be updated after late removals
437                 updateHostVisibility()
438             }
439             keysNeedRemoval.clear()
440             isUserInitiatedRemovalQueued = false
441 
442             // Update user visibility so that no extra impression will be logged when
443             // activeMediaIndex resets to 0
444             if (this::updateUserVisibility.isInitialized) {
445                 updateUserVisibility()
446             }
447 
448             // Let's reset our scroll position
449             mediaCarouselScrollHandler.scrollToStart()
450         }
451         visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback)
452         mediaManager.addListener(
453             object : MediaDataManager.Listener {
454                 override fun onMediaDataLoaded(
455                     key: String,
456                     oldKey: String?,
457                     data: MediaData,
458                     immediately: Boolean,
459                     receivedSmartspaceCardLatency: Int,
460                     isSsReactivated: Boolean,
461                 ) {
462                     debugLogger.logMediaLoaded(key, data.active)
463                     val onUiExecutionEnd =
464                         if (mediaControlsUmoInflationInBackground()) {
465                             Runnable {
466                                 if (immediately) {
467                                     updateHostVisibility()
468                                 }
469                             }
470                         } else {
471                             null
472                         }
473                     if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated, onUiExecutionEnd)) {
474                         // Log card received if a new resumable media card is added
475                         MediaPlayerData.getMediaPlayer(key)?.let {
476                             logSmartspaceCardReported(
477                                 759, // SMARTSPACE_CARD_RECEIVED
478                                 it.mSmartspaceId,
479                                 it.mUid,
480                                 surfaces =
481                                     intArrayOf(
482                                         SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
483                                         SSPACE_CARD_REPORTED__LOCKSCREEN,
484                                         SSPACE_CARD_REPORTED__DREAM_OVERLAY,
485                                     ),
486                                 rank = MediaPlayerData.getMediaPlayerIndex(key),
487                             )
488                         }
489                         if (
490                             mediaCarouselScrollHandler.visibleToUser &&
491                                 mediaCarouselScrollHandler.visibleMediaIndex ==
492                                     MediaPlayerData.getMediaPlayerIndex(key)
493                         ) {
494                             logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
495                         }
496                     } else if (receivedSmartspaceCardLatency != 0) {
497                         // Log resume card received if resumable media card is reactivated and
498                         // resume card is ranked first
499                         MediaPlayerData.players().forEachIndexed { index, it ->
500                             if (it.recommendationViewHolder == null) {
501                                 it.mSmartspaceId =
502                                     SmallHash.hash(
503                                         it.mUid + systemClock.currentTimeMillis().toInt()
504                                     )
505                                 it.mIsImpressed = false
506 
507                                 logSmartspaceCardReported(
508                                     759, // SMARTSPACE_CARD_RECEIVED
509                                     it.mSmartspaceId,
510                                     it.mUid,
511                                     surfaces =
512                                         intArrayOf(
513                                             SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
514                                             SSPACE_CARD_REPORTED__LOCKSCREEN,
515                                             SSPACE_CARD_REPORTED__DREAM_OVERLAY,
516                                         ),
517                                     rank = index,
518                                     receivedLatencyMillis = receivedSmartspaceCardLatency,
519                                 )
520                             }
521                         }
522                         // If media container area already visible to the user, log impression for
523                         // reactivated card.
524                         if (
525                             mediaCarouselScrollHandler.visibleToUser &&
526                                 !mediaCarouselScrollHandler.qsExpanded
527                         ) {
528                             logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
529                         }
530                     }
531 
532                     val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
533                     if (canRemove && !Utils.useMediaResumption(context)) {
534                         // This media control is both paused and timed out, and the resumption
535                         // setting is off - let's remove it
536                         if (isReorderingAllowed) {
537                             onMediaDataRemoved(key, userInitiated = MediaPlayerData.isSwipedAway)
538                         } else {
539                             isUserInitiatedRemovalQueued = MediaPlayerData.isSwipedAway
540                             keysNeedRemoval.add(key)
541                         }
542                     } else {
543                         keysNeedRemoval.remove(key)
544                     }
545                     MediaPlayerData.isSwipedAway = false
546                 }
547 
548                 override fun onSmartspaceMediaDataLoaded(
549                     key: String,
550                     data: SmartspaceMediaData,
551                     shouldPrioritize: Boolean,
552                 ) {
553                     debugLogger.logRecommendationLoaded(key, data.isActive)
554                     // Log the case where the hidden media carousel with the existed inactive resume
555                     // media is shown by the Smartspace signal.
556                     if (data.isActive) {
557                         val hasActivatedExistedResumeMedia =
558                             !mediaManager.hasActiveMedia() &&
559                                 mediaManager.hasAnyMedia() &&
560                                 shouldPrioritize
561                         if (hasActivatedExistedResumeMedia) {
562                             // Log resume card received if resumable media card is reactivated and
563                             // recommendation card is valid and ranked first
564                             MediaPlayerData.players().forEachIndexed { index, it ->
565                                 if (it.recommendationViewHolder == null) {
566                                     it.mSmartspaceId =
567                                         SmallHash.hash(
568                                             it.mUid + systemClock.currentTimeMillis().toInt()
569                                         )
570                                     it.mIsImpressed = false
571 
572                                     logSmartspaceCardReported(
573                                         759, // SMARTSPACE_CARD_RECEIVED
574                                         it.mSmartspaceId,
575                                         it.mUid,
576                                         surfaces =
577                                             intArrayOf(
578                                                 SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
579                                                 SSPACE_CARD_REPORTED__LOCKSCREEN,
580                                                 SSPACE_CARD_REPORTED__DREAM_OVERLAY,
581                                             ),
582                                         rank = index,
583                                         receivedLatencyMillis =
584                                             (systemClock.currentTimeMillis() -
585                                                     data.headphoneConnectionTimeMillis)
586                                                 .toInt(),
587                                     )
588                                 }
589                             }
590                         }
591                         addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
592                         MediaPlayerData.getMediaPlayer(key)?.let {
593                             logSmartspaceCardReported(
594                                 759, // SMARTSPACE_CARD_RECEIVED
595                                 it.mSmartspaceId,
596                                 it.mUid,
597                                 surfaces =
598                                     intArrayOf(
599                                         SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
600                                         SSPACE_CARD_REPORTED__LOCKSCREEN,
601                                         SSPACE_CARD_REPORTED__DREAM_OVERLAY,
602                                     ),
603                                 rank = MediaPlayerData.getMediaPlayerIndex(key),
604                                 receivedLatencyMillis =
605                                     (systemClock.currentTimeMillis() -
606                                             data.headphoneConnectionTimeMillis)
607                                         .toInt(),
608                             )
609                         }
610                         if (
611                             mediaCarouselScrollHandler.visibleToUser &&
612                                 mediaCarouselScrollHandler.visibleMediaIndex ==
613                                     MediaPlayerData.getMediaPlayerIndex(key)
614                         ) {
615                             logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
616                         }
617                     } else {
618                         if (!mediaFlags.isPersistentSsCardEnabled()) {
619                             // Handle update to inactive as a removal
620                             onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
621                         } else {
622                             addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
623                         }
624                     }
625                     MediaPlayerData.isSwipedAway = false
626                 }
627 
628                 override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
629                     debugLogger.logMediaRemoved(key, userInitiated)
630                     removePlayer(key, userInitiated = userInitiated)
631                 }
632 
633                 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
634                     debugLogger.logRecommendationRemoved(key, immediately)
635                     if (immediately || isReorderingAllowed) {
636                         removePlayer(key)
637                         if (!immediately) {
638                             // Although it wasn't requested, we were able to process the removal
639                             // immediately since reordering is allowed. So, notify hosts to update
640                             updateHostVisibility()
641                         }
642                     } else {
643                         keysNeedRemoval.add(key)
644                     }
645                 }
646             }
647         )
648     }
649 
650     private fun inflateSettingsButton() {
651         val settings =
652             LayoutInflater.from(context)
653                 .inflate(R.layout.media_carousel_settings_button, mediaFrame, false) as View
654         if (this::settingsButton.isInitialized) {
655             mediaFrame.removeView(settingsButton)
656         }
657         settingsButton = settings
658         mediaFrame.addView(settingsButton)
659         mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
660         settingsButton.setOnClickListener {
661             logger.logCarouselSettings()
662             activityStarter.startActivity(settingsIntent, /* dismissShade= */ true)
663         }
664     }
665 
666     private fun inflateMediaCarousel(): ViewGroup {
667         val mediaCarousel =
668             LayoutInflater.from(context)
669                 .inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup
670         // Because this is inflated when not attached to the true view hierarchy, it resolves some
671         // potential issues to force that the layout direction is defined by the locale
672         // (rather than inherited from the parent, which would resolve to LTR when unattached).
673         mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
674         return mediaCarousel
675     }
676 
677     private fun hideMediaCarousel() {
678         mediaCarousel.visibility = View.GONE
679     }
680 
681     private fun showMediaCarousel() {
682         mediaCarousel.visibility = View.VISIBLE
683     }
684 
685     @VisibleForTesting
686     internal fun listenForAnyStateToGoneKeyguardTransition(scope: CoroutineScope): Job {
687         return scope.launch {
688             keyguardTransitionInteractor
689                 .isFinishedIn(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
690                 .filter { it }
691                 .collect {
692                     showMediaCarousel()
693                     updateHostVisibility()
694                 }
695         }
696     }
697 
698     @VisibleForTesting
699     internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job {
700         return scope.launch {
701             keyguardTransitionInteractor
702                 .transition(Edge.create(to = LOCKSCREEN))
703                 .filter { it.transitionState == TransitionState.FINISHED }
704                 .collect {
705                     if (!allowMediaPlayerOnLockScreen) {
706                         updateHostVisibility()
707                     }
708                 }
709         }
710     }
711 
712     @VisibleForTesting
713     internal fun listenForLockscreenSettingChanges(scope: CoroutineScope): Job {
714         return scope.launch {
715             secureSettings
716                 .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
717                 // query to get initial value
718                 .onStart { emit(Unit) }
719                 .map { getMediaLockScreenSetting() }
720                 .distinctUntilChanged()
721                 .flowOn(backgroundDispatcher)
722                 .collectLatest {
723                     allowMediaPlayerOnLockScreen = it
724                     updateHostVisibility()
725                 }
726         }
727     }
728 
729     private fun listenForMediaItemsChanges(scope: CoroutineScope): Job {
730         return scope.launch {
731             mediaCarouselViewModel.mediaItems.collectLatest {
732                 val diffUtilCallback = MediaViewModelCallback(commonViewModels, it)
733                 val listUpdateCallback =
734                     MediaViewModelListUpdateCallback(
735                         old = commonViewModels,
736                         new = it,
737                         onAdded = this@MediaCarouselController::onAdded,
738                         onUpdated = this@MediaCarouselController::onUpdated,
739                         onRemoved = this@MediaCarouselController::onRemoved,
740                         onMoved = this@MediaCarouselController::onMoved,
741                     )
742                 DiffUtil.calculateDiff(diffUtilCallback).dispatchUpdatesTo(listUpdateCallback)
743                 setNewViewModelsList(it)
744 
745                 // Update host visibility when media changes.
746                 merge(
747                         mediaCarouselViewModel.hasAnyMediaOrRecommendations,
748                         mediaCarouselViewModel.hasActiveMediaOrRecommendations,
749                     )
750                     .collect { updateHostVisibility() }
751             }
752         }
753     }
754 
755     private fun onAdded(
756         commonViewModel: MediaCommonViewModel,
757         position: Int,
758         configChanged: Boolean = false,
759     ) {
760         val viewController = mediaViewControllerFactory.get()
761         viewController.sizeChangedListener = this::updateCarouselDimensions
762         val lp =
763             LinearLayout.LayoutParams(
764                 ViewGroup.LayoutParams.MATCH_PARENT,
765                 ViewGroup.LayoutParams.WRAP_CONTENT,
766             )
767         when (commonViewModel) {
768             is MediaCommonViewModel.MediaControl -> {
769                 val viewHolder = MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
770                 viewController.widthInSceneContainerPx = widthInSceneContainerPx
771                 viewController.heightInSceneContainerPx = heightInSceneContainerPx
772                 viewController.attachPlayer(viewHolder)
773                 viewController.mediaViewHolder?.player?.layoutParams = lp
774                 if (configChanged) {
775                     commonViewModel.controlViewModel.onMediaConfigChanged()
776                 }
777                 MediaControlViewBinder.bind(
778                     viewHolder,
779                     commonViewModel.controlViewModel,
780                     viewController,
781                     falsingManager,
782                     backgroundDispatcher,
783                     mainDispatcher,
784                 )
785                 mediaContent.addView(viewHolder.player, position)
786                 controllerById[commonViewModel.instanceId.toString()] = viewController
787             }
788             is MediaCommonViewModel.MediaRecommendations -> {
789                 val viewHolder =
790                     RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)
791                 viewController.attachRecommendations(viewHolder)
792                 viewController.recommendationViewHolder?.recommendations?.layoutParams = lp
793                 MediaRecommendationsViewBinder.bind(
794                     viewHolder,
795                     commonViewModel.recsViewModel,
796                     viewController,
797                     falsingManager,
798                     backgroundDispatcher,
799                     mainDispatcher,
800                 )
801                 mediaContent.addView(viewHolder.recommendations, position)
802                 controllerById[commonViewModel.key] = viewController
803             }
804         }
805         onAddOrUpdateVisibleToUserCard(position, isMediaCardUpdate = false)
806         viewController.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded)
807         updateViewControllerToState(viewController, noAnimation = true)
808         updatePageIndicator()
809         if (
810             commonViewModel is MediaCommonViewModel.MediaControl && commonViewModel.isMediaFromRec
811         ) {
812             mediaCarouselScrollHandler.scrollToPlayer(
813                 mediaCarouselScrollHandler.visibleMediaIndex,
814                 destIndex = 0,
815             )
816         }
817         mediaCarouselScrollHandler.onPlayersChanged()
818         mediaFrame.requiresRemeasuring = true
819         commonViewModel.onAdded(commonViewModel)
820     }
821 
822     private fun onUpdated(commonViewModel: MediaCommonViewModel, position: Int) {
823         commonViewModel.onUpdated(commonViewModel)
824         updatePageIndicator()
825         mediaCarouselScrollHandler.onPlayersChanged()
826         onAddOrUpdateVisibleToUserCard(
827             position,
828             commonViewModel is MediaCommonViewModel.MediaControl,
829         )
830     }
831 
832     private fun onRemoved(commonViewModel: MediaCommonViewModel) {
833         val id =
834             when (commonViewModel) {
835                 is MediaCommonViewModel.MediaControl -> commonViewModel.instanceId.toString()
836                 is MediaCommonViewModel.MediaRecommendations -> commonViewModel.key
837             }
838         controllerById.remove(id)?.let {
839             when (commonViewModel) {
840                 is MediaCommonViewModel.MediaControl -> {
841                     mediaCarouselScrollHandler.onPrePlayerRemoved(it.mediaViewHolder!!.player)
842                     mediaContent.removeView(it.mediaViewHolder!!.player)
843                 }
844                 is MediaCommonViewModel.MediaRecommendations -> {
845                     mediaContent.removeView(it.recommendationViewHolder!!.recommendations)
846                 }
847             }
848             it.onDestroy()
849             mediaCarouselScrollHandler.onPlayersChanged()
850             updatePageIndicator()
851             commonViewModel.onRemoved(true)
852         }
853     }
854 
855     private fun onMoved(commonViewModel: MediaCommonViewModel, from: Int, to: Int) {
856         val id =
857             when (commonViewModel) {
858                 is MediaCommonViewModel.MediaControl -> commonViewModel.instanceId.toString()
859                 is MediaCommonViewModel.MediaRecommendations -> commonViewModel.key
860             }
861         controllerById[id]?.let {
862             mediaContent.removeViewAt(from)
863             when (commonViewModel) {
864                 is MediaCommonViewModel.MediaControl -> {
865                     mediaContent.addView(it.mediaViewHolder!!.player, to)
866                 }
867                 is MediaCommonViewModel.MediaRecommendations -> {
868                     mediaContent.addView(it.recommendationViewHolder!!.recommendations, to)
869                 }
870             }
871         }
872         updatePageIndicator()
873         mediaCarouselScrollHandler.onPlayersChanged()
874     }
875 
876     private fun onAddOrUpdateVisibleToUserCard(position: Int, isMediaCardUpdate: Boolean) {
877         if (
878             mediaCarouselScrollHandler.visibleToUser &&
879                 mediaCarouselScrollHandler.visibleMediaIndex == position
880         ) {
881             mediaCarouselViewModel.onCardVisibleToUser(
882                 mediaCarouselScrollHandler.qsExpanded,
883                 mediaCarouselScrollHandler.visibleMediaIndex,
884                 currentEndLocation,
885                 isMediaCardUpdate,
886             )
887         }
888     }
889 
890     private fun setNewViewModelsList(viewModels: List<MediaCommonViewModel>) {
891         commonViewModels.clear()
892         commonViewModels.addAll(viewModels)
893 
894         // Ensure we only show the needed UMOs in media carousel.
895         val viewIds =
896             viewModels
897                 .map { mediaCommonViewModel ->
898                     when (mediaCommonViewModel) {
899                         is MediaCommonViewModel.MediaControl ->
900                             mediaCommonViewModel.instanceId.toString()
901                         is MediaCommonViewModel.MediaRecommendations -> mediaCommonViewModel.key
902                     }
903                 }
904                 .toHashSet()
905         controllerById
906             .filter { !viewIds.contains(it.key) }
907             .forEach {
908                 mediaCarouselScrollHandler.onPrePlayerRemoved(it.value.mediaViewHolder?.player)
909                 mediaContent.removeView(it.value.mediaViewHolder?.player)
910                 mediaContent.removeView(it.value.recommendationViewHolder?.recommendations)
911                 it.value.onDestroy()
912                 mediaCarouselScrollHandler.onPlayersChanged()
913                 updatePageIndicator()
914             }
915     }
916 
917     private suspend fun getMediaLockScreenSetting(): Boolean {
918         return withContext(backgroundDispatcher) {
919             secureSettings.getBoolForUser(
920                 Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
921                 true,
922                 UserHandle.USER_CURRENT,
923             )
924         }
925     }
926 
927     fun setSceneContainerSize(width: Int, height: Int) {
928         if (width == widthInSceneContainerPx && height == heightInSceneContainerPx) {
929             return
930         }
931         if (width <= 0 || height <= 0) {
932             // reject as invalid
933             return
934         }
935         widthInSceneContainerPx = width
936         heightInSceneContainerPx = height
937         mediaCarouselScrollHandler.playerWidthPlusPadding =
938             width + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
939         updatePlayers(recreateMedia = true)
940     }
941 
942     /** Return true if the carousel should be hidden because lockscreen is currently visible */
943     fun isLockedAndHidden(): Boolean {
944         val isOnLockscreen =
945             if (SceneContainerFlag.isEnabled) {
946                 !deviceEntryInteractor.isDeviceEntered.value
947             } else {
948                 !isOnGone.value
949             }
950         return !allowMediaPlayerOnLockScreen && isOnLockscreen
951     }
952 
953     private fun reorderAllPlayers(
954         previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?,
955         key: String? = null,
956     ) {
957         mediaContent.removeAllViews()
958         for (mediaPlayer in MediaPlayerData.players()) {
959             mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) }
960                 ?: mediaPlayer.recommendationViewHolder?.let {
961                     mediaContent.addView(it.recommendations)
962                 }
963         }
964         mediaCarouselScrollHandler.onPlayersChanged()
965         MediaPlayerData.updateVisibleMediaPlayers()
966         // Automatically scroll to the active player if needed
967         if (shouldScrollToKey) {
968             shouldScrollToKey = false
969             val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1
970             if (mediaIndex != -1) {
971                 previousVisiblePlayerKey?.let {
972                     val previousVisibleIndex =
973                         MediaPlayerData.playerKeys().indexOfFirst { key -> it == key }
974                     mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex)
975                 } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
976             }
977         } else if (isRtl && mediaContent.childCount > 0) {
978             // In RTL, Scroll to the first player as it is the rightmost player in media carousel.
979             mediaCarouselScrollHandler.scrollToPlayer(destIndex = 0)
980         }
981         // Check postcondition: mediaContent should have the same number of children as there
982         // are
983         // elements in mediaPlayers.
984         if (MediaPlayerData.players().size != mediaContent.childCount) {
985             Log.e(
986                 TAG,
987                 "Size of players list and number of views in carousel are out of sync. " +
988                     "Players size is ${MediaPlayerData.players().size}. " +
989                     "View count is ${mediaContent.childCount}.",
990             )
991         }
992     }
993 
994     // Returns true if new player is added
995     private fun addOrUpdatePlayer(
996         key: String,
997         oldKey: String?,
998         data: MediaData,
999         isSsReactivated: Boolean,
1000         onUiExecutionEnd: Runnable? = null,
1001     ): Boolean =
1002         traceSection("MediaCarouselController#addOrUpdatePlayer") {
1003             MediaPlayerData.moveIfExists(oldKey, key)
1004             val existingPlayer = MediaPlayerData.getMediaPlayer(key)
1005             val curVisibleMediaKey =
1006                 MediaPlayerData.visiblePlayerKeys()
1007                     .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
1008             if (mediaControlsUmoInflationInBackground()) {
1009                 if (existingPlayer == null) {
1010                     bgExecutor.execute {
1011                         val mediaViewHolder = createMediaViewHolderInBg()
1012                         // Add the new player in the main thread.
1013                         uiExecutor.execute {
1014                             setupNewPlayer(
1015                                 key,
1016                                 data,
1017                                 isSsReactivated,
1018                                 curVisibleMediaKey,
1019                                 mediaViewHolder,
1020                             )
1021                             updatePageIndicator()
1022                             mediaCarouselScrollHandler.onPlayersChanged()
1023                             mediaFrame.requiresRemeasuring = true
1024                             onUiExecutionEnd?.run()
1025                         }
1026                     }
1027                 } else {
1028                     updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
1029                     updatePageIndicator()
1030                     mediaCarouselScrollHandler.onPlayersChanged()
1031                     mediaFrame.requiresRemeasuring = true
1032                     onUiExecutionEnd?.run()
1033                 }
1034             } else {
1035                 if (existingPlayer == null) {
1036                     val mediaViewHolder =
1037                         MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
1038                     setupNewPlayer(key, data, isSsReactivated, curVisibleMediaKey, mediaViewHolder)
1039                 } else {
1040                     updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
1041                 }
1042                 updatePageIndicator()
1043                 mediaCarouselScrollHandler.onPlayersChanged()
1044                 mediaFrame.requiresRemeasuring = true
1045                 onUiExecutionEnd?.run()
1046             }
1047             return existingPlayer == null
1048         }
1049 
1050     private fun updatePlayer(
1051         key: String,
1052         data: MediaData,
1053         isSsReactivated: Boolean,
1054         curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
1055         existingPlayer: MediaControlPanel,
1056     ) {
1057         existingPlayer.bindPlayer(data, key)
1058         MediaPlayerData.addMediaPlayer(
1059             key,
1060             data,
1061             existingPlayer,
1062             systemClock,
1063             isSsReactivated,
1064             debugLogger,
1065         )
1066         val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
1067         // In case of recommendations hits.
1068         // Check the playing status of media player and the package name.
1069         // To make sure we scroll to the right app's media player.
1070         if (
1071             isReorderingAllowed ||
1072                 shouldScrollToKey && data.isPlaying == true && packageName == data.packageName
1073         ) {
1074             reorderAllPlayers(curVisibleMediaKey, key)
1075         } else {
1076             needsReordering = true
1077         }
1078     }
1079 
1080     private fun setupNewPlayer(
1081         key: String,
1082         data: MediaData,
1083         isSsReactivated: Boolean,
1084         curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
1085         mediaViewHolder: MediaViewHolder,
1086     ) {
1087         val newPlayer = mediaControlPanelFactory.get()
1088         newPlayer.attachPlayer(mediaViewHolder)
1089         newPlayer.mediaViewController.sizeChangedListener =
1090             this@MediaCarouselController::updateCarouselDimensions
1091         val lp =
1092             LinearLayout.LayoutParams(
1093                 ViewGroup.LayoutParams.MATCH_PARENT,
1094                 ViewGroup.LayoutParams.WRAP_CONTENT,
1095             )
1096         newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
1097         newPlayer.bindPlayer(data, key)
1098         newPlayer.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded)
1099         MediaPlayerData.addMediaPlayer(
1100             key,
1101             data,
1102             newPlayer,
1103             systemClock,
1104             isSsReactivated,
1105             debugLogger,
1106         )
1107         updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true)
1108         // Media data added from a recommendation card should starts playing.
1109         if ((shouldScrollToKey && data.isPlaying == true) || (!shouldScrollToKey && data.active)) {
1110             reorderAllPlayers(curVisibleMediaKey, key)
1111         } else {
1112             needsReordering = true
1113         }
1114     }
1115 
1116     @WorkerThread
1117     private fun createMediaViewHolderInBg(): MediaViewHolder {
1118         return MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
1119     }
1120 
1121     private fun addSmartspaceMediaRecommendations(
1122         key: String,
1123         data: SmartspaceMediaData,
1124         shouldPrioritize: Boolean,
1125     ) =
1126         traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
1127             if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
1128             MediaPlayerData.getMediaPlayer(key)?.let {
1129                 if (mediaFlags.isPersistentSsCardEnabled()) {
1130                     // The card exists, but could have changed active state, so update for sorting
1131                     MediaPlayerData.addMediaRecommendation(
1132                         key,
1133                         data,
1134                         it,
1135                         shouldPrioritize,
1136                         systemClock,
1137                         debugLogger,
1138                         update = true,
1139                     )
1140                 }
1141                 Log.w(TAG, "Skip adding smartspace target in carousel")
1142                 return
1143             }
1144 
1145             val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
1146             existingSmartspaceMediaKey?.let {
1147                 val removedPlayer =
1148                     removePlayer(existingSmartspaceMediaKey, dismissMediaData = false)
1149                 removedPlayer?.run {
1150                     debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey)
1151                     onDestroy()
1152                 }
1153             }
1154 
1155             val newRecs = mediaControlPanelFactory.get()
1156             newRecs.attachRecommendation(
1157                 RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)
1158             )
1159             newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
1160             val lp =
1161                 LinearLayout.LayoutParams(
1162                     ViewGroup.LayoutParams.MATCH_PARENT,
1163                     ViewGroup.LayoutParams.WRAP_CONTENT,
1164                 )
1165             newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
1166             newRecs.bindRecommendation(data)
1167             val curVisibleMediaKey =
1168                 MediaPlayerData.visiblePlayerKeys()
1169                     .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
1170             MediaPlayerData.addMediaRecommendation(
1171                 key,
1172                 data,
1173                 newRecs,
1174                 shouldPrioritize,
1175                 systemClock,
1176                 debugLogger,
1177             )
1178             updateViewControllerToState(newRecs.mediaViewController, noAnimation = true)
1179             reorderAllPlayers(curVisibleMediaKey)
1180             updatePageIndicator()
1181             mediaFrame.requiresRemeasuring = true
1182             // Check postcondition: mediaContent should have the same number of children as there
1183             // are
1184             // elements in mediaPlayers.
1185             if (MediaPlayerData.players().size != mediaContent.childCount) {
1186                 Log.e(
1187                     TAG,
1188                     "Size of players list and number of views in carousel are out of sync. " +
1189                         "Players size is ${MediaPlayerData.players().size}. " +
1190                         "View count is ${mediaContent.childCount}.",
1191                 )
1192             }
1193         }
1194 
1195     fun removePlayer(
1196         key: String,
1197         dismissMediaData: Boolean = true,
1198         dismissRecommendation: Boolean = true,
1199         userInitiated: Boolean = false,
1200     ): MediaControlPanel? {
1201         if (key == MediaPlayerData.smartspaceMediaKey()) {
1202             MediaPlayerData.smartspaceMediaData?.let {
1203                 logger.logRecommendationRemoved(it.packageName, it.instanceId)
1204             }
1205         }
1206         val removed =
1207             MediaPlayerData.removeMediaPlayer(key, dismissMediaData || dismissRecommendation)
1208         return removed?.apply {
1209             mediaCarouselScrollHandler.onPrePlayerRemoved(removed.mediaViewHolder?.player)
1210             mediaContent.removeView(removed.mediaViewHolder?.player)
1211             mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
1212             removed.onDestroy()
1213             mediaCarouselScrollHandler.onPlayersChanged()
1214             updatePageIndicator()
1215 
1216             if (dismissMediaData) {
1217                 // Inform the media manager of a potentially late dismissal
1218                 mediaManager.dismissMediaData(key, delay = 0L, userInitiated = userInitiated)
1219             }
1220             if (dismissRecommendation) {
1221                 // Inform the media manager of a potentially late dismissal
1222                 mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
1223             }
1224         }
1225     }
1226 
1227     private fun updatePlayers(recreateMedia: Boolean) {
1228         if (SceneContainerFlag.isEnabled) {
1229             updateMediaPlayers(recreateMedia)
1230             return
1231         }
1232         pageIndicator.tintList =
1233             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
1234         val previousVisibleKey =
1235             MediaPlayerData.visiblePlayerKeys()
1236                 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
1237         val onUiExecutionEnd = Runnable {
1238             if (recreateMedia) {
1239                 reorderAllPlayers(previousVisibleKey)
1240             }
1241         }
1242 
1243         val mediaDataList = MediaPlayerData.mediaData()
1244         // Do not loop through the original list of media data because the re-addition of media data
1245         // is being executed in background thread.
1246         mediaDataList.forEach { (key, data, isSsMediaRec) ->
1247             if (isSsMediaRec) {
1248                 val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
1249                 removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
1250                 smartspaceMediaData?.let {
1251                     addSmartspaceMediaRecommendations(
1252                         it.targetId,
1253                         it,
1254                         MediaPlayerData.shouldPrioritizeSs,
1255                     )
1256                 }
1257                 onUiExecutionEnd.run()
1258             } else {
1259                 val isSsReactivated = MediaPlayerData.isSsReactivated(key)
1260                 if (recreateMedia) {
1261                     removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
1262                 }
1263                 addOrUpdatePlayer(
1264                     key = key,
1265                     oldKey = null,
1266                     data = data,
1267                     isSsReactivated = isSsReactivated,
1268                     onUiExecutionEnd = onUiExecutionEnd,
1269                 )
1270             }
1271         }
1272     }
1273 
1274     private fun updateMediaPlayers(recreateMedia: Boolean) {
1275         pageIndicator.tintList =
1276             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
1277         if (recreateMedia) {
1278             mediaContent.removeAllViews()
1279             commonViewModels.forEachIndexed { index, viewModel ->
1280                 when (viewModel) {
1281                     is MediaCommonViewModel.MediaControl ->
1282                         controllerById[viewModel.instanceId.toString()]?.onDestroy()
1283                     is MediaCommonViewModel.MediaRecommendations ->
1284                         controllerById[viewModel.key]?.onDestroy()
1285                 }
1286                 onAdded(viewModel, index, configChanged = true)
1287             }
1288         }
1289     }
1290 
1291     private fun updatePageIndicator() {
1292         val numPages = mediaContent.getChildCount()
1293         pageIndicator.setNumPages(numPages)
1294         if (numPages == 1) {
1295             pageIndicator.setLocation(0f)
1296         }
1297         updatePageIndicatorAlpha()
1298     }
1299 
1300     /**
1301      * Set a new interpolated state for all players. This is a state that is usually controlled by a
1302      * finger movement where the user drags from one state to the next.
1303      *
1304      * @param startLocation the start location of our state or -1 if this is directly set
1305      * @param endLocation the ending location of our state.
1306      * @param progress the progress of the transition between startLocation and endlocation. If
1307      *
1308      * ```
1309      *                 this is not a guided transformation, this will be 1.0f
1310      * @param immediately
1311      * ```
1312      *
1313      * should this state be applied immediately, canceling all animations?
1314      */
1315     fun setCurrentState(
1316         @MediaLocation startLocation: Int,
1317         @MediaLocation endLocation: Int,
1318         progress: Float,
1319         immediately: Boolean,
1320     ) {
1321         if (
1322             startLocation != currentStartLocation ||
1323                 endLocation != currentEndLocation ||
1324                 progress != currentTransitionProgress ||
1325                 immediately
1326         ) {
1327             currentStartLocation = startLocation
1328             currentEndLocation = endLocation
1329             currentTransitionProgress = progress
1330             if (!SceneContainerFlag.isEnabled) {
1331                 for (mediaPlayer in MediaPlayerData.players()) {
1332                     updateViewControllerToState(mediaPlayer.mediaViewController, immediately)
1333                 }
1334             } else {
1335                 controllerById.values.forEach { updateViewControllerToState(it, immediately) }
1336             }
1337             maybeResetSettingsCog()
1338             updatePageIndicatorAlpha()
1339         }
1340     }
1341 
1342     @VisibleForTesting
1343     fun updatePageIndicatorAlpha() {
1344         val hostStates = mediaHostStatesManager.mediaHostStates
1345         val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
1346         val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
1347         val startAlpha = if (startIsVisible) 1.0f else 0.0f
1348         // when squishing in split shade, only use endState, which keeps changing
1349         // to provide squishFraction
1350         val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
1351         val endAlpha =
1352             (if (endIsVisible) 1.0f else 0.0f) *
1353                 calculateAlpha(
1354                     squishFraction,
1355                     (pageIndicator.translationY + pageIndicator.height) /
1356                         mediaCarousel.measuredHeight,
1357                     1F,
1358                 )
1359         var alpha = 1.0f
1360         if (!endIsVisible || !startIsVisible) {
1361             var progress = currentTransitionProgress
1362             if (!endIsVisible) {
1363                 progress = 1.0f - progress
1364             }
1365             // Let's fade in quickly at the end where the view is visible
1366             progress =
1367                 MathUtils.constrain(MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), 0.0f, 1.0f)
1368             alpha = MathUtils.lerp(startAlpha, endAlpha, progress)
1369         }
1370         pageIndicator.alpha = alpha
1371     }
1372 
1373     private fun updatePageIndicatorLocation() {
1374         // Update the location of the page indicator, carousel clipping
1375         val translationX =
1376             if (isRtl) {
1377                 (pageIndicator.width - currentCarouselWidth) / 2.0f
1378             } else {
1379                 (currentCarouselWidth - pageIndicator.width) / 2.0f
1380             }
1381         pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
1382         val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
1383         pageIndicator.translationY =
1384             (mediaCarousel.measuredHeight - pageIndicator.height - layoutParams.bottomMargin)
1385                 .toFloat()
1386     }
1387 
1388     /** Update listening to seekbar. */
1389     private fun updateSeekbarListening(visibleToUser: Boolean) {
1390         if (!SceneContainerFlag.isEnabled) {
1391             for (player in MediaPlayerData.players()) {
1392                 player.setListening(visibleToUser && currentlyExpanded)
1393             }
1394         } else {
1395             controllerById.values.forEach { it.setListening(visibleToUser && currentlyExpanded) }
1396         }
1397     }
1398 
1399     /** Update the dimension of this carousel. */
1400     private fun updateCarouselDimensions() {
1401         var width = 0
1402         var height = 0
1403         if (!SceneContainerFlag.isEnabled) {
1404             for (mediaPlayer in MediaPlayerData.players()) {
1405                 val controller = mediaPlayer.mediaViewController
1406                 // When transitioning the view to gone, the view gets smaller, but the translation
1407                 // Doesn't, let's add the translation
1408                 width = Math.max(width, controller.currentWidth + controller.translationX.toInt())
1409                 height =
1410                     Math.max(height, controller.currentHeight + controller.translationY.toInt())
1411             }
1412         } else {
1413             controllerById.values.forEach {
1414                 // When transitioning the view to gone, the view gets smaller, but the translation
1415                 // Doesn't, let's add the translation
1416                 width = Math.max(width, it.currentWidth + it.translationX.toInt())
1417                 height = Math.max(height, it.currentHeight + it.translationY.toInt())
1418             }
1419         }
1420         if (width != currentCarouselWidth || height != currentCarouselHeight) {
1421             currentCarouselWidth = width
1422             currentCarouselHeight = height
1423             mediaCarouselScrollHandler.setCarouselBounds(
1424                 currentCarouselWidth,
1425                 currentCarouselHeight,
1426             )
1427             updatePageIndicatorLocation()
1428             updatePageIndicatorAlpha()
1429         }
1430     }
1431 
1432     private fun maybeResetSettingsCog() {
1433         val hostStates = mediaHostStatesManager.mediaHostStates
1434         val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia ?: true
1435         val startShowsActive =
1436             hostStates[currentStartLocation]?.showsOnlyActiveMedia ?: endShowsActive
1437         val startDisablePagination = hostStates[currentStartLocation]?.disablePagination ?: false
1438         val endDisablePagination = hostStates[currentEndLocation]?.disablePagination ?: false
1439 
1440         if (
1441             currentlyShowingOnlyActive != endShowsActive ||
1442                 currentlyDisablePagination != endDisablePagination ||
1443                 ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
1444                     (startShowsActive != endShowsActive ||
1445                         startDisablePagination != endDisablePagination))
1446         ) {
1447             // Whenever we're transitioning from between differing states or the endstate differs
1448             // we reset the translation
1449             currentlyShowingOnlyActive = endShowsActive
1450             currentlyDisablePagination = endDisablePagination
1451             mediaCarouselScrollHandler.resetTranslation(animate = true)
1452         }
1453     }
1454 
1455     private fun updateViewControllerToState(
1456         viewController: MediaViewController,
1457         noAnimation: Boolean,
1458     ) {
1459         viewController.setCurrentState(
1460             startLocation = currentStartLocation,
1461             endLocation = currentEndLocation,
1462             transitionProgress = currentTransitionProgress,
1463             applyImmediately = noAnimation,
1464         )
1465     }
1466 
1467     /**
1468      * The desired location of this view has changed. We should remeasure the view to match the new
1469      * bounds and kick off bounds animations if necessary. If an animation is happening, an
1470      * animation is kicked of externally, which sets a new current state until we reach the
1471      * targetState.
1472      *
1473      * @param desiredLocation the location we're going to
1474      * @param desiredHostState the target state we're transitioning to
1475      * @param animate should this be animated
1476      */
1477     fun onDesiredLocationChanged(
1478         desiredLocation: Int,
1479         desiredHostState: MediaHostState?,
1480         animate: Boolean,
1481         duration: Long = 200,
1482         startDelay: Long = 0,
1483     ) =
1484         traceSection("MediaCarouselController#onDesiredLocationChanged") {
1485             desiredHostState?.let {
1486                 if (this.desiredLocation != desiredLocation) {
1487                     // Only log an event when location changes
1488                     bgExecutor.execute { logger.logCarouselPosition(desiredLocation) }
1489                 }
1490 
1491                 // This is a hosting view, let's remeasure our players
1492                 this.desiredLocation = desiredLocation
1493                 this.desiredHostState = it
1494                 currentlyExpanded = it.expansion > 0
1495 
1496                 val shouldCloseGuts =
1497                     !currentlyExpanded &&
1498                         !mediaManager.hasActiveMediaOrRecommendation() &&
1499                         desiredHostState.showsOnlyActiveMedia
1500 
1501                 if (!SceneContainerFlag.isEnabled) {
1502                     for (mediaPlayer in MediaPlayerData.players()) {
1503                         if (animate) {
1504                             mediaPlayer.mediaViewController.animatePendingStateChange(
1505                                 duration = duration,
1506                                 delay = startDelay,
1507                             )
1508                         }
1509                         if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) {
1510                             mediaPlayer.closeGuts(!animate)
1511                         }
1512 
1513                         mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
1514                     }
1515                 } else {
1516                     controllerById.values.forEach { controller ->
1517                         if (animate) {
1518                             controller.animatePendingStateChange(duration, startDelay)
1519                         }
1520                         if (shouldCloseGuts && controller.isGutsVisible) {
1521                             controller.closeGuts(!animate)
1522                         }
1523 
1524                         controller.onLocationPreChange(desiredLocation)
1525                     }
1526                 }
1527                 mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
1528                 mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
1529                 val nowVisible = it.visible
1530                 if (nowVisible != playersVisible) {
1531                     playersVisible = nowVisible
1532                     if (nowVisible) {
1533                         mediaCarouselScrollHandler.resetTranslation()
1534                     }
1535                 }
1536                 updateCarouselSize()
1537             }
1538         }
1539 
1540     fun closeGuts(immediate: Boolean = true) {
1541         if (!SceneContainerFlag.isEnabled) {
1542             MediaPlayerData.players().forEach { it.closeGuts(immediate) }
1543         } else {
1544             controllerById.values.forEach { it.closeGuts(immediate) }
1545         }
1546     }
1547 
1548     /** Update the size of the carousel, remeasuring it if necessary. */
1549     private fun updateCarouselSize() {
1550         val width = desiredHostState?.measurementInput?.width ?: 0
1551         val height = desiredHostState?.measurementInput?.height ?: 0
1552         if (
1553             width != carouselMeasureWidth && width != 0 ||
1554                 height != carouselMeasureHeight && height != 0
1555         ) {
1556             carouselMeasureWidth = width
1557             carouselMeasureHeight = height
1558             val playerWidthPlusPadding =
1559                 carouselMeasureWidth +
1560                     context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
1561             // Let's remeasure the carousel
1562             val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
1563             val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
1564             mediaCarousel.measure(widthSpec, heightSpec)
1565             mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
1566             // Update the padding after layout; view widths are used in RTL to calculate scrollX
1567             mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
1568         }
1569     }
1570 
1571     /** Log the user impression for media card at visibleMediaIndex. */
1572     fun logSmartspaceImpression(qsExpanded: Boolean) {
1573         if (SceneContainerFlag.isEnabled) {
1574             mediaCarouselViewModel.onCardVisibleToUser(
1575                 qsExpanded,
1576                 mediaCarouselScrollHandler.visibleMediaIndex,
1577                 currentEndLocation,
1578             )
1579             return
1580         }
1581         val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
1582         if (MediaPlayerData.players().size > visibleMediaIndex) {
1583             val mediaControlPanel = MediaPlayerData.getMediaControlPanel(visibleMediaIndex)
1584             val hasActiveMediaOrRecommendationCard =
1585                 MediaPlayerData.hasActiveMediaOrRecommendationCard()
1586             if (!hasActiveMediaOrRecommendationCard && !qsExpanded) {
1587                 // Skip logging if on LS or QQS, and there is no active media card
1588                 return
1589             }
1590             mediaControlPanel?.let {
1591                 logSmartspaceCardReported(
1592                     800, // SMARTSPACE_CARD_SEEN
1593                     it.mSmartspaceId,
1594                     it.mUid,
1595                     intArrayOf(it.surfaceForSmartspaceLogging),
1596                 )
1597                 it.mIsImpressed = true
1598             }
1599         }
1600     }
1601 
1602     /**
1603      * Log Smartspace events
1604      *
1605      * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN)
1606      * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new
1607      *   instanceId
1608      * @param uid uid for the application that media comes from
1609      * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when
1610      *   the event happened
1611      * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1
1612      *   for tapping on card but not on any media item, 0 for first media item, 1 for second, etc.
1613      * @param interactedSubcardCardinality how many media items were shown to the user when there is
1614      *   user interaction
1615      * @param rank the rank for media card in the media carousel, starting from 0
1616      * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency
1617      *   between headphone connection to sysUI displays media recommendation card
1618      * @param isSwipeToDismiss whether is to log swipe-to-dismiss event
1619      */
1620     @JvmOverloads
1621     fun logSmartspaceCardReported(
1622         eventId: Int,
1623         instanceId: Int,
1624         uid: Int,
1625         surfaces: IntArray,
1626         interactedSubcardRank: Int = 0,
1627         interactedSubcardCardinality: Int = 0,
1628         rank: Int = mediaCarouselScrollHandler.visibleMediaIndex,
1629         receivedLatencyMillis: Int = 0,
1630         isSwipeToDismiss: Boolean = false,
1631     ) {
1632         if (MediaPlayerData.players().size <= rank) {
1633             return
1634         }
1635 
1636         val mediaControlKey = MediaPlayerData.visiblePlayerKeys().elementAt(rank)
1637         // Only log media resume card when Smartspace data is available
1638         if (
1639             !mediaControlKey.isSsMediaRec &&
1640                 !mediaManager.isRecommendationActive() &&
1641                 MediaPlayerData.smartspaceMediaData == null
1642         ) {
1643             return
1644         }
1645 
1646         val cardinality = mediaContent.getChildCount()
1647         surfaces.forEach { surface ->
1648             SysUiStatsLog.write(
1649                 SMARTSPACE_CARD_REPORTED,
1650                 eventId,
1651                 instanceId,
1652                 // Deprecated, replaced with AiAi feature type so we don't need to create logging
1653                 // card type for each new feature.
1654                 SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD,
1655                 surface,
1656                 // Use -1 as rank value to indicate user swipe to dismiss the card
1657                 if (isSwipeToDismiss) -1 else rank,
1658                 cardinality,
1659                 if (mediaControlKey.isSsMediaRec) {
1660                     15 // MEDIA_RECOMMENDATION
1661                 } else if (mediaControlKey.isSsReactivated) {
1662                     43 // MEDIA_RESUME_SS_ACTIVATED
1663                 } else {
1664                     31
1665                 }, // MEDIA_RESUME
1666                 uid,
1667                 interactedSubcardRank,
1668                 interactedSubcardCardinality,
1669                 receivedLatencyMillis,
1670                 null, // Media cards cannot have subcards.
1671                 null, // Media cards don't have dimensions today.
1672             )
1673 
1674             if (DEBUG) {
1675                 Log.d(
1676                     TAG,
1677                     "Log Smartspace card event id: $eventId instance id: $instanceId" +
1678                         " surface: $surface rank: $rank cardinality: $cardinality " +
1679                         "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " +
1680                         "isSsReactivated: ${mediaControlKey.isSsReactivated}" +
1681                         "uid: $uid " +
1682                         "interactedSubcardRank: $interactedSubcardRank " +
1683                         "interactedSubcardCardinality: $interactedSubcardCardinality " +
1684                         "received_latency_millis: $receivedLatencyMillis",
1685                 )
1686             }
1687         }
1688     }
1689 
1690     @VisibleForTesting
1691     fun onSwipeToDismiss() {
1692         if (SceneContainerFlag.isEnabled) {
1693             mediaCarouselViewModel.onSwipeToDismiss(currentEndLocation)
1694             return
1695         }
1696         MediaPlayerData.players().forEachIndexed { index, it ->
1697             if (it.mIsImpressed) {
1698                 logSmartspaceCardReported(
1699                     SMARTSPACE_CARD_DISMISS_EVENT,
1700                     it.mSmartspaceId,
1701                     it.mUid,
1702                     intArrayOf(it.surfaceForSmartspaceLogging),
1703                     rank = index,
1704                     isSwipeToDismiss = true,
1705                 )
1706                 // Reset card impressed state when swipe to dismissed
1707                 it.mIsImpressed = false
1708             }
1709         }
1710         MediaPlayerData.isSwipedAway = true
1711         logger.logSwipeDismiss()
1712         mediaManager.onSwipeToDismiss()
1713     }
1714 
1715     fun getCurrentVisibleMediaContentIntent(): PendingIntent? {
1716         return MediaPlayerData.playerKeys()
1717             .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
1718             ?.data
1719             ?.clickIntent
1720     }
1721 
1722     override fun dump(pw: PrintWriter, args: Array<out String>) {
1723         pw.apply {
1724             println("keysNeedRemoval: $keysNeedRemoval")
1725             println("dataKeys: ${MediaPlayerData.dataKeys()}")
1726             println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}")
1727             println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}")
1728             println("commonViewModels: $commonViewModels")
1729             println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
1730             println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
1731             println("current size: $currentCarouselWidth x $currentCarouselHeight")
1732             println("location: $desiredLocation")
1733             println(
1734                 "state: ${desiredHostState?.expansion}, " +
1735                     "only active ${desiredHostState?.showsOnlyActiveMedia}"
1736             )
1737             println("isSwipedAway: ${MediaPlayerData.isSwipedAway}")
1738             println("allowMediaPlayerOnLockScreen: $allowMediaPlayerOnLockScreen")
1739         }
1740     }
1741 }
1742 
1743 @VisibleForTesting
1744 internal object MediaPlayerData {
1745     private val EMPTY =
1746         MediaData(
1747             userId = -1,
1748             initialized = false,
1749             app = null,
1750             appIcon = null,
1751             artist = null,
1752             song = null,
1753             artwork = null,
1754             actions = emptyList(),
1755             actionsToShowInCompact = emptyList(),
1756             packageName = "INVALID",
1757             token = null,
1758             clickIntent = null,
1759             device = null,
1760             active = true,
1761             resumeAction = null,
1762             instanceId = InstanceId.fakeInstanceId(-1),
1763             appUid = -1,
1764         )
1765 
1766     // Whether should prioritize Smartspace card.
1767     internal var shouldPrioritizeSs: Boolean = false
1768         private set
1769 
1770     internal var smartspaceMediaData: SmartspaceMediaData? = null
1771         private set
1772 
1773     data class MediaSortKey(
1774         val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation.
1775         val data: MediaData,
1776         val key: String,
1777         val updateTime: Long = 0,
1778         val isSsReactivated: Boolean = false,
1779     )
1780 
1781     private val comparator =
<lambda>null1782         compareByDescending<MediaSortKey> {
1783                 it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL
1784             }
<lambda>null1785             .thenByDescending {
1786                 it.data.isPlaying == true &&
1787                     it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL
1788             }
<lambda>null1789             .thenByDescending { it.data.active }
<lambda>null1790             .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec }
<lambda>null1791             .thenByDescending { !it.data.resumption }
<lambda>null1792             .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
<lambda>null1793             .thenByDescending { it.data.lastActive }
<lambda>null1794             .thenByDescending { it.updateTime }
<lambda>null1795             .thenByDescending { it.data.notificationKey }
1796 
1797     private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
1798     private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
1799 
1800     // A map that tracks order of visible media players before they get reordered.
1801     private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>()
1802 
1803     // Whether the user swiped away the carousel since its last update
1804     internal var isSwipedAway: Boolean = false
1805 
addMediaPlayernull1806     fun addMediaPlayer(
1807         key: String,
1808         data: MediaData,
1809         player: MediaControlPanel,
1810         clock: SystemClock,
1811         isSsReactivated: Boolean,
1812         debugLogger: MediaCarouselControllerLogger? = null,
1813     ) {
1814         val removedPlayer = removeMediaPlayer(key)
1815         if (removedPlayer != null && removedPlayer != player) {
1816             debugLogger?.logPotentialMemoryLeak(key)
1817             removedPlayer.onDestroy()
1818         }
1819         val sortKey =
1820             MediaSortKey(
1821                 isSsMediaRec = false,
1822                 data,
1823                 key,
1824                 clock.currentTimeMillis(),
1825                 isSsReactivated = isSsReactivated,
1826             )
1827         mediaData.put(key, sortKey)
1828         mediaPlayers.put(sortKey, player)
1829         visibleMediaPlayers.put(key, sortKey)
1830     }
1831 
addMediaRecommendationnull1832     fun addMediaRecommendation(
1833         key: String,
1834         data: SmartspaceMediaData,
1835         player: MediaControlPanel,
1836         shouldPrioritize: Boolean,
1837         clock: SystemClock,
1838         debugLogger: MediaCarouselControllerLogger? = null,
1839         update: Boolean = false,
1840     ) {
1841         shouldPrioritizeSs = shouldPrioritize
1842         val removedPlayer = removeMediaPlayer(key)
1843         if (!update && removedPlayer != null && removedPlayer != player) {
1844             debugLogger?.logPotentialMemoryLeak(key)
1845             removedPlayer.onDestroy()
1846         }
1847         val sortKey =
1848             MediaSortKey(
1849                 isSsMediaRec = true,
1850                 EMPTY.copy(active = data.isActive, isPlaying = false),
1851                 key,
1852                 clock.currentTimeMillis(),
1853                 isSsReactivated = true,
1854             )
1855         mediaData.put(key, sortKey)
1856         mediaPlayers.put(sortKey, player)
1857         visibleMediaPlayers.put(key, sortKey)
1858         smartspaceMediaData = data
1859     }
1860 
moveIfExistsnull1861     fun moveIfExists(
1862         oldKey: String?,
1863         newKey: String,
1864         debugLogger: MediaCarouselControllerLogger? = null,
1865     ) {
1866         if (oldKey == null || oldKey == newKey) {
1867             return
1868         }
1869 
1870         mediaData.remove(oldKey)?.let {
1871             // MediaPlayer should not be visible
1872             // no need to set isDismissed flag.
1873             val removedPlayer = removeMediaPlayer(newKey)
1874             removedPlayer?.run {
1875                 debugLogger?.logPotentialMemoryLeak(newKey)
1876                 onDestroy()
1877             }
1878             mediaData.put(newKey, it)
1879         }
1880     }
1881 
getMediaControlPanelnull1882     fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? {
1883         return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex))
1884     }
1885 
getMediaPlayernull1886     fun getMediaPlayer(key: String): MediaControlPanel? {
1887         return mediaData.get(key)?.let { mediaPlayers.get(it) }
1888     }
1889 
getMediaPlayerIndexnull1890     fun getMediaPlayerIndex(key: String): Int {
1891         val sortKey = mediaData.get(key)
1892         mediaPlayers.entries.forEachIndexed { index, e ->
1893             if (e.key == sortKey) {
1894                 return index
1895             }
1896         }
1897         return -1
1898     }
1899 
1900     /**
1901      * Removes media player given the key.
1902      *
1903      * @param isDismissed determines whether the media player is removed from the carousel.
1904      */
removeMediaPlayernull1905     fun removeMediaPlayer(key: String, isDismissed: Boolean = false) =
1906         mediaData.remove(key)?.let {
1907             if (it.isSsMediaRec) {
1908                 smartspaceMediaData = null
1909             }
1910             if (isDismissed) {
1911                 visibleMediaPlayers.remove(key)
1912             }
1913             mediaPlayers.remove(it)
1914         }
1915 
mediaDatanull1916     fun mediaData() =
1917         mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
1918 
dataKeysnull1919     fun dataKeys() = mediaData.keys
1920 
1921     fun players() = mediaPlayers.values
1922 
1923     fun playerKeys() = mediaPlayers.keys
1924 
1925     fun visiblePlayerKeys() = visibleMediaPlayers.values
1926 
1927     /** Returns the index of the first non-timeout media. */
1928     fun firstActiveMediaIndex(): Int {
1929         mediaPlayers.entries.forEachIndexed { index, e ->
1930             if (!e.key.isSsMediaRec && e.key.data.active) {
1931                 return index
1932             }
1933         }
1934         return -1
1935     }
1936 
1937     /** Returns the existing Smartspace target id. */
smartspaceMediaKeynull1938     fun smartspaceMediaKey(): String? {
1939         mediaData.entries.forEach { e ->
1940             if (e.value.isSsMediaRec) {
1941                 return e.key
1942             }
1943         }
1944         return null
1945     }
1946 
1947     @VisibleForTesting
clearnull1948     fun clear() {
1949         mediaData.clear()
1950         mediaPlayers.clear()
1951         visibleMediaPlayers.clear()
1952     }
1953 
1954     /* Returns true if there is active media player card or recommendation card */
hasActiveMediaOrRecommendationCardnull1955     fun hasActiveMediaOrRecommendationCard(): Boolean {
1956         if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) {
1957             return true
1958         }
1959         if (firstActiveMediaIndex() != -1) {
1960             return true
1961         }
1962         return false
1963     }
1964 
isSsReactivatednull1965     fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false
1966 
1967     /**
1968      * This method is called when media players are reordered. To make sure we have the new version
1969      * of the order of media players visible to user.
1970      */
1971     fun updateVisibleMediaPlayers() {
1972         visibleMediaPlayers.clear()
1973         playerKeys().forEach { visibleMediaPlayers.put(it.key, it) }
1974     }
1975 }
1976