• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.media.controls.ui
18 
19 import android.app.PendingIntent
20 import android.content.Context
21 import android.content.Intent
22 import android.content.res.ColorStateList
23 import android.content.res.Configuration
24 import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
25 import android.util.Log
26 import android.util.MathUtils
27 import android.view.LayoutInflater
28 import android.view.View
29 import android.view.ViewGroup
30 import android.view.animation.PathInterpolator
31 import android.widget.LinearLayout
32 import androidx.annotation.VisibleForTesting
33 import androidx.lifecycle.Lifecycle
34 import androidx.lifecycle.repeatOnLifecycle
35 import com.android.internal.logging.InstanceId
36 import com.android.keyguard.KeyguardUpdateMonitor
37 import com.android.keyguard.KeyguardUpdateMonitorCallback
38 import com.android.systemui.Dumpable
39 import com.android.systemui.R
40 import com.android.systemui.classifier.FalsingCollector
41 import com.android.systemui.dagger.SysUISingleton
42 import com.android.systemui.dagger.qualifiers.Main
43 import com.android.systemui.dump.DumpManager
44 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
45 import com.android.systemui.keyguard.shared.model.TransitionState
46 import com.android.systemui.lifecycle.repeatWhenAttached
47 import com.android.systemui.media.controls.models.player.MediaData
48 import com.android.systemui.media.controls.models.player.MediaViewHolder
49 import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
50 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
51 import com.android.systemui.media.controls.pipeline.MediaDataManager
52 import com.android.systemui.media.controls.ui.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
53 import com.android.systemui.media.controls.util.MediaFlags
54 import com.android.systemui.media.controls.util.MediaUiEventLogger
55 import com.android.systemui.media.controls.util.SmallHash
56 import com.android.systemui.plugins.ActivityStarter
57 import com.android.systemui.plugins.FalsingManager
58 import com.android.systemui.qs.PageIndicator
59 import com.android.systemui.shared.system.SysUiStatsLog
60 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
61 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
62 import com.android.systemui.statusbar.policy.ConfigurationController
63 import com.android.systemui.util.Utils
64 import com.android.systemui.util.animation.UniqueObjectHostView
65 import com.android.systemui.util.animation.requiresRemeasuring
66 import com.android.systemui.util.concurrency.DelayableExecutor
67 import com.android.systemui.util.time.SystemClock
68 import com.android.systemui.util.traceSection
69 import java.io.PrintWriter
70 import java.util.TreeMap
71 import javax.inject.Inject
72 import javax.inject.Provider
73 import kotlinx.coroutines.CoroutineScope
74 import kotlinx.coroutines.Job
75 import kotlinx.coroutines.flow.filter
76 import kotlinx.coroutines.launch
77 
78 private const val TAG = "MediaCarouselController"
79 private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
80 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
81 
82 /**
83  * Class that is responsible for keeping the view carousel up to date. This also handles changes in
84  * state and applies them to the media carousel like the expansion.
85  */
86 @SysUISingleton
87 class MediaCarouselController
88 @Inject
89 constructor(
90     private val context: Context,
91     private val mediaControlPanelFactory: Provider<MediaControlPanel>,
92     private val visualStabilityProvider: VisualStabilityProvider,
93     private val mediaHostStatesManager: MediaHostStatesManager,
94     private val activityStarter: ActivityStarter,
95     private val systemClock: SystemClock,
96     @Main executor: DelayableExecutor,
97     private val mediaManager: MediaDataManager,
98     configurationController: ConfigurationController,
99     falsingCollector: FalsingCollector,
100     falsingManager: FalsingManager,
101     dumpManager: DumpManager,
102     private val logger: MediaUiEventLogger,
103     private val debugLogger: MediaCarouselControllerLogger,
104     private val mediaFlags: MediaFlags,
105     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
106     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
107 ) : Dumpable {
108     /** The current width of the carousel */
109     private var currentCarouselWidth: Int = 0
110 
111     /** The current height of the carousel */
112     @VisibleForTesting var currentCarouselHeight: Int = 0
113 
114     /** Are we currently showing only active players */
115     private var currentlyShowingOnlyActive: Boolean = false
116 
117     /** Is the player currently visible (at the end of the transformation */
118     private var playersVisible: Boolean = false
119     /**
120      * The desired location where we'll be at the end of the transformation. Usually this matches
121      * the end location, except when we're still waiting on a state update call.
122      */
123     @MediaLocation private var desiredLocation: Int = -1
124 
125     /**
126      * The ending location of the view where it ends when all animations and transitions have
127      * finished
128      */
129     @MediaLocation @VisibleForTesting var currentEndLocation: Int = -1
130 
131     /**
132      * The ending location of the view where it ends when all animations and transitions have
133      * finished
134      */
135     @MediaLocation private var currentStartLocation: Int = -1
136 
137     /** The progress of the transition or 1.0 if there is no transition happening */
138     private var currentTransitionProgress: Float = 1.0f
139 
140     /** The measured width of the carousel */
141     private var carouselMeasureWidth: Int = 0
142 
143     /** The measured height of the carousel */
144     private var carouselMeasureHeight: Int = 0
145     private var desiredHostState: MediaHostState? = null
146     @VisibleForTesting var mediaCarousel: MediaScrollView
147     val mediaCarouselScrollHandler: MediaCarouselScrollHandler
148     val mediaFrame: ViewGroup
149     @VisibleForTesting
150     lateinit var settingsButton: View
151         private set
152     private val mediaContent: ViewGroup
153     @VisibleForTesting var pageIndicator: PageIndicator
154     private val visualStabilityCallback: OnReorderingAllowedListener
155     private var needsReordering: Boolean = false
156     private var keysNeedRemoval = mutableSetOf<String>()
157     var shouldScrollToKey: Boolean = false
158     private var isRtl: Boolean = false
159         set(value) {
160             if (value != field) {
161                 field = value
162                 mediaFrame.layoutDirection =
163                     if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
164                 mediaCarouselScrollHandler.scrollToStart()
165             }
166         }
167 
168     @VisibleForTesting
169     var currentlyExpanded = true
170         set(value) {
171             if (field != value) {
172                 field = value
173                 updateSeekbarListening(mediaCarouselScrollHandler.visibleToUser)
174             }
175         }
176 
177     companion object {
178         val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F)
179 
180         fun calculateAlpha(
181             squishinessFraction: Float,
182             startPosition: Float,
183             endPosition: Float
184         ): Float {
185             val transformFraction =
186                 MathUtils.constrain(
187                     (squishinessFraction - startPosition) / (endPosition - startPosition),
188                     0F,
189                     1F
190                 )
191             return TRANSFORM_BEZIER.getInterpolation(transformFraction)
192         }
193     }
194 
195     private val configListener =
196         object : ConfigurationController.ConfigurationListener {
197 
198             override fun onDensityOrFontScaleChanged() {
199                 // System font changes should only happen when UMO is offscreen or a flicker may
200                 // occur
201                 updatePlayers(recreateMedia = true)
202                 inflateSettingsButton()
203             }
204 
205             override fun onThemeChanged() {
206                 updatePlayers(recreateMedia = false)
207                 inflateSettingsButton()
208             }
209 
210             override fun onConfigChanged(newConfig: Configuration?) {
211                 if (newConfig == null) return
212                 isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
213             }
214 
215             override fun onUiModeChanged() {
216                 updatePlayers(recreateMedia = false)
217                 inflateSettingsButton()
218             }
219         }
220 
221     private val keyguardUpdateMonitorCallback =
222         object : KeyguardUpdateMonitorCallback() {
223             override fun onStrongAuthStateChanged(userId: Int) {
224                 if (keyguardUpdateMonitor.isUserInLockdown(userId)) {
225                     debugLogger.logCarouselHidden()
226                     hideMediaCarousel()
227                 } else if (keyguardUpdateMonitor.isUserUnlocked(userId)) {
228                     debugLogger.logCarouselVisible()
229                     showMediaCarousel()
230                 }
231             }
232         }
233 
234     /**
235      * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility.
236      * It will be called when the container is out of view.
237      */
238     lateinit var updateUserVisibility: () -> Unit
239     lateinit var updateHostVisibility: () -> Unit
240 
241     private val isReorderingAllowed: Boolean
242         get() = visualStabilityProvider.isReorderingAllowed
243 
244     init {
245         dumpManager.registerDumpable(TAG, this)
246         mediaFrame = inflateMediaCarousel()
247         mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
248         pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
249         mediaCarouselScrollHandler =
250             MediaCarouselScrollHandler(
251                 mediaCarousel,
252                 pageIndicator,
253                 executor,
254                 this::onSwipeToDismiss,
255                 this::updatePageIndicatorLocation,
256                 this::updateSeekbarListening,
257                 this::closeGuts,
258                 falsingCollector,
259                 falsingManager,
260                 this::logSmartspaceImpression,
261                 logger
262             )
263         isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
264         inflateSettingsButton()
265         mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
266         configurationController.addCallback(configListener)
267         visualStabilityCallback = OnReorderingAllowedListener {
268             if (needsReordering) {
269                 needsReordering = false
270                 reorderAllPlayers(previousVisiblePlayerKey = null)
271             }
272 
273             keysNeedRemoval.forEach { removePlayer(it) }
274             if (keysNeedRemoval.size > 0) {
275                 // Carousel visibility may need to be updated after late removals
276                 updateHostVisibility()
277             }
278             keysNeedRemoval.clear()
279 
280             // Update user visibility so that no extra impression will be logged when
281             // activeMediaIndex resets to 0
282             if (this::updateUserVisibility.isInitialized) {
283                 updateUserVisibility()
284             }
285 
286             // Let's reset our scroll position
287             mediaCarouselScrollHandler.scrollToStart()
288         }
289         visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback)
290         mediaManager.addListener(
291             object : MediaDataManager.Listener {
292                 override fun onMediaDataLoaded(
293                     key: String,
294                     oldKey: String?,
295                     data: MediaData,
296                     immediately: Boolean,
297                     receivedSmartspaceCardLatency: Int,
298                     isSsReactivated: Boolean
299                 ) {
300                     debugLogger.logMediaLoaded(key, data.active)
301                     if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) {
302                         // Log card received if a new resumable media card is added
303                         MediaPlayerData.getMediaPlayer(key)?.let {
304                             /* ktlint-disable max-line-length */
305                             logSmartspaceCardReported(
306                                 759, // SMARTSPACE_CARD_RECEIVED
307                                 it.mSmartspaceId,
308                                 it.mUid,
309                                 surfaces =
310                                     intArrayOf(
311                                         SysUiStatsLog
312                                             .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
313                                         SysUiStatsLog
314                                             .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
315                                         SysUiStatsLog
316                                             .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
317                                     ),
318                                 rank = MediaPlayerData.getMediaPlayerIndex(key)
319                             )
320                             /* ktlint-disable max-line-length */
321                         }
322                         if (
323                             mediaCarouselScrollHandler.visibleToUser &&
324                                 mediaCarouselScrollHandler.visibleMediaIndex ==
325                                     MediaPlayerData.getMediaPlayerIndex(key)
326                         ) {
327                             logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
328                         }
329                     } else if (receivedSmartspaceCardLatency != 0) {
330                         // Log resume card received if resumable media card is reactivated and
331                         // resume card is ranked first
332                         MediaPlayerData.players().forEachIndexed { index, it ->
333                             if (it.recommendationViewHolder == null) {
334                                 it.mSmartspaceId =
335                                     SmallHash.hash(
336                                         it.mUid + systemClock.currentTimeMillis().toInt()
337                                     )
338                                 it.mIsImpressed = false
339                                 /* ktlint-disable max-line-length */
340                                 logSmartspaceCardReported(
341                                     759, // SMARTSPACE_CARD_RECEIVED
342                                     it.mSmartspaceId,
343                                     it.mUid,
344                                     surfaces =
345                                         intArrayOf(
346                                             SysUiStatsLog
347                                                 .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
348                                             SysUiStatsLog
349                                                 .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
350                                             SysUiStatsLog
351                                                 .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
352                                         ),
353                                     rank = index,
354                                     receivedLatencyMillis = receivedSmartspaceCardLatency
355                                 )
356                                 /* ktlint-disable max-line-length */
357                             }
358                         }
359                         // If media container area already visible to the user, log impression for
360                         // reactivated card.
361                         if (
362                             mediaCarouselScrollHandler.visibleToUser &&
363                                 !mediaCarouselScrollHandler.qsExpanded
364                         ) {
365                             logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
366                         }
367                     }
368 
369                     val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
370                     if (canRemove && !Utils.useMediaResumption(context)) {
371                         // This view isn't playing, let's remove this! This happens e.g. when
372                         // dismissing/timing out a view. We still have the data around because
373                         // resumption could be on, but we should save the resources and release
374                         // this.
375                         if (isReorderingAllowed) {
376                             onMediaDataRemoved(key)
377                         } else {
378                             keysNeedRemoval.add(key)
379                         }
380                     } else {
381                         keysNeedRemoval.remove(key)
382                     }
383                 }
384 
385                 override fun onSmartspaceMediaDataLoaded(
386                     key: String,
387                     data: SmartspaceMediaData,
388                     shouldPrioritize: Boolean
389                 ) {
390                     debugLogger.logRecommendationLoaded(key, data.isActive)
391                     // Log the case where the hidden media carousel with the existed inactive resume
392                     // media is shown by the Smartspace signal.
393                     if (data.isActive) {
394                         val hasActivatedExistedResumeMedia =
395                             !mediaManager.hasActiveMedia() &&
396                                 mediaManager.hasAnyMedia() &&
397                                 shouldPrioritize
398                         if (hasActivatedExistedResumeMedia) {
399                             // Log resume card received if resumable media card is reactivated and
400                             // recommendation card is valid and ranked first
401                             MediaPlayerData.players().forEachIndexed { index, it ->
402                                 if (it.recommendationViewHolder == null) {
403                                     it.mSmartspaceId =
404                                         SmallHash.hash(
405                                             it.mUid + systemClock.currentTimeMillis().toInt()
406                                         )
407                                     it.mIsImpressed = false
408                                     /* ktlint-disable max-line-length */
409                                     logSmartspaceCardReported(
410                                         759, // SMARTSPACE_CARD_RECEIVED
411                                         it.mSmartspaceId,
412                                         it.mUid,
413                                         surfaces =
414                                             intArrayOf(
415                                                 SysUiStatsLog
416                                                     .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
417                                                 SysUiStatsLog
418                                                     .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
419                                                 SysUiStatsLog
420                                                     .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
421                                             ),
422                                         rank = index,
423                                         receivedLatencyMillis =
424                                             (systemClock.currentTimeMillis() -
425                                                     data.headphoneConnectionTimeMillis)
426                                                 .toInt()
427                                     )
428                                     /* ktlint-disable max-line-length */
429                                 }
430                             }
431                         }
432                         addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
433                         MediaPlayerData.getMediaPlayer(key)?.let {
434                             /* ktlint-disable max-line-length */
435                             logSmartspaceCardReported(
436                                 759, // SMARTSPACE_CARD_RECEIVED
437                                 it.mSmartspaceId,
438                                 it.mUid,
439                                 surfaces =
440                                     intArrayOf(
441                                         SysUiStatsLog
442                                             .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
443                                         SysUiStatsLog
444                                             .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
445                                         SysUiStatsLog
446                                             .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
447                                     ),
448                                 rank = MediaPlayerData.getMediaPlayerIndex(key),
449                                 receivedLatencyMillis =
450                                     (systemClock.currentTimeMillis() -
451                                             data.headphoneConnectionTimeMillis)
452                                         .toInt()
453                             )
454                             /* ktlint-disable max-line-length */
455                         }
456                         if (
457                             mediaCarouselScrollHandler.visibleToUser &&
458                                 mediaCarouselScrollHandler.visibleMediaIndex ==
459                                     MediaPlayerData.getMediaPlayerIndex(key)
460                         ) {
461                             logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
462                         }
463                     } else {
464                         if (!mediaFlags.isPersistentSsCardEnabled()) {
465                             // Handle update to inactive as a removal
466                             onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
467                         } else {
468                             addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
469                         }
470                     }
471                 }
472 
473                 override fun onMediaDataRemoved(key: String) {
474                     debugLogger.logMediaRemoved(key)
475                     removePlayer(key)
476                 }
477 
478                 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
479                     debugLogger.logRecommendationRemoved(key, immediately)
480                     if (immediately || isReorderingAllowed) {
481                         removePlayer(key)
482                         if (!immediately) {
483                             // Although it wasn't requested, we were able to process the removal
484                             // immediately since reordering is allowed. So, notify hosts to update
485                             if (this@MediaCarouselController::updateHostVisibility.isInitialized) {
486                                 updateHostVisibility()
487                             }
488                         }
489                     } else {
490                         keysNeedRemoval.add(key)
491                     }
492                 }
493             }
494         )
495         mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
496             // The pageIndicator is not laid out yet when we get the current state update,
497             // Lets make sure we have the right dimensions
498             updatePageIndicatorLocation()
499         }
500         mediaHostStatesManager.addCallback(
501             object : MediaHostStatesManager.Callback {
502                 override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
503                     if (location == desiredLocation) {
504                         onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
505                     }
506                 }
507             }
508         )
509         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
510         mediaCarousel.repeatWhenAttached {
511             repeatOnLifecycle(Lifecycle.State.STARTED) {
512                 // A backup to show media carousel (if available) once the keyguard is gone.
513                 listenForAnyStateToGoneKeyguardTransition(this)
514             }
515         }
516     }
517 
518     private fun inflateSettingsButton() {
519         val settings =
520             LayoutInflater.from(context)
521                 .inflate(R.layout.media_carousel_settings_button, mediaFrame, false) as View
522         if (this::settingsButton.isInitialized) {
523             mediaFrame.removeView(settingsButton)
524         }
525         settingsButton = settings
526         mediaFrame.addView(settingsButton)
527         mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
528         settingsButton.setOnClickListener {
529             logger.logCarouselSettings()
530             activityStarter.startActivity(settingsIntent, true /* dismissShade */)
531         }
532     }
533 
534     private fun inflateMediaCarousel(): ViewGroup {
535         val mediaCarousel =
536             LayoutInflater.from(context)
537                 .inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup
538         // Because this is inflated when not attached to the true view hierarchy, it resolves some
539         // potential issues to force that the layout direction is defined by the locale
540         // (rather than inherited from the parent, which would resolve to LTR when unattached).
541         mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
542         return mediaCarousel
543     }
544 
545     private fun hideMediaCarousel() {
546         mediaCarousel.visibility = View.GONE
547     }
548 
549     private fun showMediaCarousel() {
550         mediaCarousel.visibility = View.VISIBLE
551     }
552 
553     @VisibleForTesting
554     internal fun listenForAnyStateToGoneKeyguardTransition(scope: CoroutineScope): Job {
555         return scope.launch {
556             keyguardTransitionInteractor.anyStateToGoneTransition
557                 .filter { it.transitionState == TransitionState.FINISHED }
558                 .collect { showMediaCarousel() }
559         }
560     }
561 
562     private fun reorderAllPlayers(
563         previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?,
564         key: String? = null
565     ) {
566         mediaContent.removeAllViews()
567         for (mediaPlayer in MediaPlayerData.players()) {
568             mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) }
569                 ?: mediaPlayer.recommendationViewHolder?.let {
570                     mediaContent.addView(it.recommendations)
571                 }
572         }
573         mediaCarouselScrollHandler.onPlayersChanged()
574         MediaPlayerData.updateVisibleMediaPlayers()
575         // Automatically scroll to the active player if needed
576         if (shouldScrollToKey) {
577             shouldScrollToKey = false
578             val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1
579             if (mediaIndex != -1) {
580                 previousVisiblePlayerKey?.let {
581                     val previousVisibleIndex =
582                         MediaPlayerData.playerKeys().indexOfFirst { key -> it == key }
583                     mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex)
584                 }
585                     ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
586             }
587         }
588         // Check postcondition: mediaContent should have the same number of children as there
589         // are
590         // elements in mediaPlayers.
591         if (MediaPlayerData.players().size != mediaContent.childCount) {
592             Log.e(
593                 TAG,
594                 "Size of players list and number of views in carousel are out of sync. " +
595                     "Players size is ${MediaPlayerData.players().size}. " +
596                     "View count is ${mediaContent.childCount}."
597             )
598         }
599     }
600 
601     // Returns true if new player is added
602     private fun addOrUpdatePlayer(
603         key: String,
604         oldKey: String?,
605         data: MediaData,
606         isSsReactivated: Boolean
607     ): Boolean =
608         traceSection("MediaCarouselController#addOrUpdatePlayer") {
609             MediaPlayerData.moveIfExists(oldKey, key)
610             val existingPlayer = MediaPlayerData.getMediaPlayer(key)
611             val curVisibleMediaKey =
612                 MediaPlayerData.visiblePlayerKeys()
613                     .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
614             if (existingPlayer == null) {
615                 val newPlayer = mediaControlPanelFactory.get()
616                 newPlayer.attachPlayer(
617                     MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
618                 )
619                 newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
620                 val lp =
621                     LinearLayout.LayoutParams(
622                         ViewGroup.LayoutParams.MATCH_PARENT,
623                         ViewGroup.LayoutParams.WRAP_CONTENT
624                     )
625                 newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
626                 newPlayer.bindPlayer(data, key)
627                 newPlayer.setListening(
628                     mediaCarouselScrollHandler.visibleToUser && currentlyExpanded
629                 )
630                 MediaPlayerData.addMediaPlayer(
631                     key,
632                     data,
633                     newPlayer,
634                     systemClock,
635                     isSsReactivated,
636                     debugLogger
637                 )
638                 updatePlayerToState(newPlayer, noAnimation = true)
639                 // Media data added from a recommendation card should starts playing.
640                 if (
641                     (shouldScrollToKey && data.isPlaying == true) ||
642                         (!shouldScrollToKey && data.active)
643                 ) {
644                     reorderAllPlayers(curVisibleMediaKey, key)
645                 } else {
646                     needsReordering = true
647                 }
648             } else {
649                 existingPlayer.bindPlayer(data, key)
650                 MediaPlayerData.addMediaPlayer(
651                     key,
652                     data,
653                     existingPlayer,
654                     systemClock,
655                     isSsReactivated,
656                     debugLogger
657                 )
658                 val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
659                 // In case of recommendations hits.
660                 // Check the playing status of media player and the package name.
661                 // To make sure we scroll to the right app's media player.
662                 if (
663                     isReorderingAllowed ||
664                         shouldScrollToKey &&
665                             data.isPlaying == true &&
666                             packageName == data.packageName
667                 ) {
668                     reorderAllPlayers(curVisibleMediaKey, key)
669                 } else {
670                     needsReordering = true
671                 }
672             }
673             updatePageIndicator()
674             mediaCarouselScrollHandler.onPlayersChanged()
675             mediaFrame.requiresRemeasuring = true
676             return existingPlayer == null
677         }
678 
679     private fun addSmartspaceMediaRecommendations(
680         key: String,
681         data: SmartspaceMediaData,
682         shouldPrioritize: Boolean
683     ) =
684         traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
685             if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
686             MediaPlayerData.getMediaPlayer(key)?.let {
687                 if (mediaFlags.isPersistentSsCardEnabled()) {
688                     // The card exists, but could have changed active state, so update for sorting
689                     MediaPlayerData.addMediaRecommendation(
690                         key,
691                         data,
692                         it,
693                         shouldPrioritize,
694                         systemClock,
695                         debugLogger,
696                         update = true,
697                     )
698                 }
699                 Log.w(TAG, "Skip adding smartspace target in carousel")
700                 return
701             }
702 
703             val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
704             existingSmartspaceMediaKey?.let {
705                 val removedPlayer =
706                     removePlayer(existingSmartspaceMediaKey, dismissMediaData = false)
707                 removedPlayer?.run {
708                     debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey)
709                 }
710             }
711 
712             val newRecs = mediaControlPanelFactory.get()
713             newRecs.attachRecommendation(
714                 RecommendationViewHolder.create(
715                     LayoutInflater.from(context),
716                     mediaContent,
717                     mediaFlags.isRecommendationCardUpdateEnabled()
718                 )
719             )
720             newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
721             val lp =
722                 LinearLayout.LayoutParams(
723                     ViewGroup.LayoutParams.MATCH_PARENT,
724                     ViewGroup.LayoutParams.WRAP_CONTENT
725                 )
726             newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
727             newRecs.bindRecommendation(data)
728             val curVisibleMediaKey =
729                 MediaPlayerData.visiblePlayerKeys()
730                     .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
731             MediaPlayerData.addMediaRecommendation(
732                 key,
733                 data,
734                 newRecs,
735                 shouldPrioritize,
736                 systemClock,
737                 debugLogger,
738             )
739             updatePlayerToState(newRecs, noAnimation = true)
740             reorderAllPlayers(curVisibleMediaKey)
741             updatePageIndicator()
742             mediaFrame.requiresRemeasuring = true
743             // Check postcondition: mediaContent should have the same number of children as there
744             // are
745             // elements in mediaPlayers.
746             if (MediaPlayerData.players().size != mediaContent.childCount) {
747                 Log.e(
748                     TAG,
749                     "Size of players list and number of views in carousel are out of sync. " +
750                         "Players size is ${MediaPlayerData.players().size}. " +
751                         "View count is ${mediaContent.childCount}."
752                 )
753             }
754         }
755 
756     fun removePlayer(
757         key: String,
758         dismissMediaData: Boolean = true,
759         dismissRecommendation: Boolean = true
760     ): MediaControlPanel? {
761         if (key == MediaPlayerData.smartspaceMediaKey()) {
762             MediaPlayerData.smartspaceMediaData?.let {
763                 logger.logRecommendationRemoved(it.packageName, it.instanceId)
764             }
765         }
766         val removed =
767             MediaPlayerData.removeMediaPlayer(key, dismissMediaData || dismissRecommendation)
768         return removed?.apply {
769             mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
770             mediaContent.removeView(removed.mediaViewHolder?.player)
771             mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
772             removed.onDestroy()
773             mediaCarouselScrollHandler.onPlayersChanged()
774             updatePageIndicator()
775 
776             if (dismissMediaData) {
777                 // Inform the media manager of a potentially late dismissal
778                 mediaManager.dismissMediaData(key, delay = 0L)
779             }
780             if (dismissRecommendation) {
781                 // Inform the media manager of a potentially late dismissal
782                 mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
783             }
784         }
785     }
786 
787     private fun updatePlayers(recreateMedia: Boolean) {
788         pageIndicator.tintList =
789             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
790         val previousVisibleKey =
791             MediaPlayerData.visiblePlayerKeys()
792                 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
793 
794         MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
795             if (isSsMediaRec) {
796                 val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
797                 removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
798                 smartspaceMediaData?.let {
799                     addSmartspaceMediaRecommendations(
800                         it.targetId,
801                         it,
802                         MediaPlayerData.shouldPrioritizeSs
803                     )
804                 }
805             } else {
806                 val isSsReactivated = MediaPlayerData.isSsReactivated(key)
807                 if (recreateMedia) {
808                     removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
809                 }
810                 addOrUpdatePlayer(
811                     key = key,
812                     oldKey = null,
813                     data = data,
814                     isSsReactivated = isSsReactivated
815                 )
816             }
817             if (recreateMedia) {
818                 reorderAllPlayers(previousVisibleKey)
819             }
820         }
821     }
822 
823     private fun updatePageIndicator() {
824         val numPages = mediaContent.getChildCount()
825         pageIndicator.setNumPages(numPages)
826         if (numPages == 1) {
827             pageIndicator.setLocation(0f)
828         }
829         updatePageIndicatorAlpha()
830     }
831 
832     /**
833      * Set a new interpolated state for all players. This is a state that is usually controlled by a
834      * finger movement where the user drags from one state to the next.
835      *
836      * @param startLocation the start location of our state or -1 if this is directly set
837      * @param endLocation the ending location of our state.
838      * @param progress the progress of the transition between startLocation and endlocation. If
839      *
840      * ```
841      *                 this is not a guided transformation, this will be 1.0f
842      * @param immediately
843      * ```
844      *
845      * should this state be applied immediately, canceling all animations?
846      */
847     fun setCurrentState(
848         @MediaLocation startLocation: Int,
849         @MediaLocation endLocation: Int,
850         progress: Float,
851         immediately: Boolean
852     ) {
853         if (
854             startLocation != currentStartLocation ||
855                 endLocation != currentEndLocation ||
856                 progress != currentTransitionProgress ||
857                 immediately
858         ) {
859             currentStartLocation = startLocation
860             currentEndLocation = endLocation
861             currentTransitionProgress = progress
862             for (mediaPlayer in MediaPlayerData.players()) {
863                 updatePlayerToState(mediaPlayer, immediately)
864             }
865             maybeResetSettingsCog()
866             updatePageIndicatorAlpha()
867         }
868     }
869 
870     @VisibleForTesting
871     fun updatePageIndicatorAlpha() {
872         val hostStates = mediaHostStatesManager.mediaHostStates
873         val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
874         val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
875         val startAlpha = if (startIsVisible) 1.0f else 0.0f
876         // when squishing in split shade, only use endState, which keeps changing
877         // to provide squishFraction
878         val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
879         val endAlpha =
880             (if (endIsVisible) 1.0f else 0.0f) *
881                 calculateAlpha(
882                     squishFraction,
883                     (pageIndicator.translationY + pageIndicator.height) /
884                         mediaCarousel.measuredHeight,
885                     1F
886                 )
887         var alpha = 1.0f
888         if (!endIsVisible || !startIsVisible) {
889             var progress = currentTransitionProgress
890             if (!endIsVisible) {
891                 progress = 1.0f - progress
892             }
893             // Let's fade in quickly at the end where the view is visible
894             progress =
895                 MathUtils.constrain(MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), 0.0f, 1.0f)
896             alpha = MathUtils.lerp(startAlpha, endAlpha, progress)
897         }
898         pageIndicator.alpha = alpha
899     }
900 
901     private fun updatePageIndicatorLocation() {
902         // Update the location of the page indicator, carousel clipping
903         val translationX =
904             if (isRtl) {
905                 (pageIndicator.width - currentCarouselWidth) / 2.0f
906             } else {
907                 (currentCarouselWidth - pageIndicator.width) / 2.0f
908             }
909         pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
910         val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
911         pageIndicator.translationY =
912             (mediaCarousel.measuredHeight - pageIndicator.height - layoutParams.bottomMargin)
913                 .toFloat()
914     }
915 
916     /** Update listening to seekbar. */
917     private fun updateSeekbarListening(visibleToUser: Boolean) {
918         for (player in MediaPlayerData.players()) {
919             player.setListening(visibleToUser && currentlyExpanded)
920         }
921     }
922 
923     /** Update the dimension of this carousel. */
924     private fun updateCarouselDimensions() {
925         var width = 0
926         var height = 0
927         for (mediaPlayer in MediaPlayerData.players()) {
928             val controller = mediaPlayer.mediaViewController
929             // When transitioning the view to gone, the view gets smaller, but the translation
930             // Doesn't, let's add the translation
931             width = Math.max(width, controller.currentWidth + controller.translationX.toInt())
932             height = Math.max(height, controller.currentHeight + controller.translationY.toInt())
933         }
934         if (width != currentCarouselWidth || height != currentCarouselHeight) {
935             currentCarouselWidth = width
936             currentCarouselHeight = height
937             mediaCarouselScrollHandler.setCarouselBounds(
938                 currentCarouselWidth,
939                 currentCarouselHeight
940             )
941             updatePageIndicatorLocation()
942             updatePageIndicatorAlpha()
943         }
944     }
945 
946     private fun maybeResetSettingsCog() {
947         val hostStates = mediaHostStatesManager.mediaHostStates
948         val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia ?: true
949         val startShowsActive =
950             hostStates[currentStartLocation]?.showsOnlyActiveMedia ?: endShowsActive
951         if (
952             currentlyShowingOnlyActive != endShowsActive ||
953                 ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
954                     startShowsActive != endShowsActive)
955         ) {
956             // Whenever we're transitioning from between differing states or the endstate differs
957             // we reset the translation
958             currentlyShowingOnlyActive = endShowsActive
959             mediaCarouselScrollHandler.resetTranslation(animate = true)
960         }
961     }
962 
963     private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
964         mediaPlayer.mediaViewController.setCurrentState(
965             startLocation = currentStartLocation,
966             endLocation = currentEndLocation,
967             transitionProgress = currentTransitionProgress,
968             applyImmediately = noAnimation
969         )
970     }
971 
972     /**
973      * The desired location of this view has changed. We should remeasure the view to match the new
974      * bounds and kick off bounds animations if necessary. If an animation is happening, an
975      * animation is kicked of externally, which sets a new current state until we reach the
976      * targetState.
977      *
978      * @param desiredLocation the location we're going to
979      * @param desiredHostState the target state we're transitioning to
980      * @param animate should this be animated
981      */
982     fun onDesiredLocationChanged(
983         desiredLocation: Int,
984         desiredHostState: MediaHostState?,
985         animate: Boolean,
986         duration: Long = 200,
987         startDelay: Long = 0
988     ) =
989         traceSection("MediaCarouselController#onDesiredLocationChanged") {
990             desiredHostState?.let {
991                 if (this.desiredLocation != desiredLocation) {
992                     // Only log an event when location changes
993                     logger.logCarouselPosition(desiredLocation)
994                 }
995 
996                 // This is a hosting view, let's remeasure our players
997                 this.desiredLocation = desiredLocation
998                 this.desiredHostState = it
999                 currentlyExpanded = it.expansion > 0
1000 
1001                 val shouldCloseGuts =
1002                     !currentlyExpanded &&
1003                         !mediaManager.hasActiveMediaOrRecommendation() &&
1004                         desiredHostState.showsOnlyActiveMedia
1005 
1006                 for (mediaPlayer in MediaPlayerData.players()) {
1007                     if (animate) {
1008                         mediaPlayer.mediaViewController.animatePendingStateChange(
1009                             duration = duration,
1010                             delay = startDelay
1011                         )
1012                     }
1013                     if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) {
1014                         mediaPlayer.closeGuts(!animate)
1015                     }
1016 
1017                     mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
1018                 }
1019                 mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
1020                 mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
1021                 val nowVisible = it.visible
1022                 if (nowVisible != playersVisible) {
1023                     playersVisible = nowVisible
1024                     if (nowVisible) {
1025                         mediaCarouselScrollHandler.resetTranslation()
1026                     }
1027                 }
1028                 updateCarouselSize()
1029             }
1030         }
1031 
1032     fun closeGuts(immediate: Boolean = true) {
1033         MediaPlayerData.players().forEach { it.closeGuts(immediate) }
1034     }
1035 
1036     /** Update the size of the carousel, remeasuring it if necessary. */
1037     private fun updateCarouselSize() {
1038         val width = desiredHostState?.measurementInput?.width ?: 0
1039         val height = desiredHostState?.measurementInput?.height ?: 0
1040         if (
1041             width != carouselMeasureWidth && width != 0 ||
1042                 height != carouselMeasureHeight && height != 0
1043         ) {
1044             carouselMeasureWidth = width
1045             carouselMeasureHeight = height
1046             val playerWidthPlusPadding =
1047                 carouselMeasureWidth +
1048                     context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
1049             // Let's remeasure the carousel
1050             val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
1051             val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
1052             mediaCarousel.measure(widthSpec, heightSpec)
1053             mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
1054             // Update the padding after layout; view widths are used in RTL to calculate scrollX
1055             mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
1056         }
1057     }
1058 
1059     /** Log the user impression for media card at visibleMediaIndex. */
1060     fun logSmartspaceImpression(qsExpanded: Boolean) {
1061         val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
1062         if (MediaPlayerData.players().size > visibleMediaIndex) {
1063             val mediaControlPanel = MediaPlayerData.getMediaControlPanel(visibleMediaIndex)
1064             val hasActiveMediaOrRecommendationCard =
1065                 MediaPlayerData.hasActiveMediaOrRecommendationCard()
1066             if (!hasActiveMediaOrRecommendationCard && !qsExpanded) {
1067                 // Skip logging if on LS or QQS, and there is no active media card
1068                 return
1069             }
1070             mediaControlPanel?.let {
1071                 logSmartspaceCardReported(
1072                     800, // SMARTSPACE_CARD_SEEN
1073                     it.mSmartspaceId,
1074                     it.mUid,
1075                     intArrayOf(it.surfaceForSmartspaceLogging)
1076                 )
1077                 it.mIsImpressed = true
1078             }
1079         }
1080     }
1081 
1082     @JvmOverloads
1083     /**
1084      * Log Smartspace events
1085      *
1086      * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN)
1087      * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new
1088      *   instanceId
1089      * @param uid uid for the application that media comes from
1090      * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when
1091      *   the event happened
1092      * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1
1093      *   for tapping on card but not on any media item, 0 for first media item, 1 for second, etc.
1094      * @param interactedSubcardCardinality how many media items were shown to the user when there is
1095      *   user interaction
1096      * @param rank the rank for media card in the media carousel, starting from 0
1097      * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency
1098      *   between headphone connection to sysUI displays media recommendation card
1099      * @param isSwipeToDismiss whether is to log swipe-to-dismiss event
1100      */
1101     fun logSmartspaceCardReported(
1102         eventId: Int,
1103         instanceId: Int,
1104         uid: Int,
1105         surfaces: IntArray,
1106         interactedSubcardRank: Int = 0,
1107         interactedSubcardCardinality: Int = 0,
1108         rank: Int = mediaCarouselScrollHandler.visibleMediaIndex,
1109         receivedLatencyMillis: Int = 0,
1110         isSwipeToDismiss: Boolean = false
1111     ) {
1112         if (MediaPlayerData.players().size <= rank) {
1113             return
1114         }
1115 
1116         val mediaControlKey = MediaPlayerData.visiblePlayerKeys().elementAt(rank)
1117         // Only log media resume card when Smartspace data is available
1118         if (
1119             !mediaControlKey.isSsMediaRec &&
1120                 !mediaManager.smartspaceMediaData.isActive &&
1121                 MediaPlayerData.smartspaceMediaData == null
1122         ) {
1123             return
1124         }
1125 
1126         val cardinality = mediaContent.getChildCount()
1127         surfaces.forEach { surface ->
1128             /* ktlint-disable max-line-length */
1129             SysUiStatsLog.write(
1130                 SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
1131                 eventId,
1132                 instanceId,
1133                 // Deprecated, replaced with AiAi feature type so we don't need to create logging
1134                 // card type for each new feature.
1135                 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD,
1136                 surface,
1137                 // Use -1 as rank value to indicate user swipe to dismiss the card
1138                 if (isSwipeToDismiss) -1 else rank,
1139                 cardinality,
1140                 if (mediaControlKey.isSsMediaRec) 15 // MEDIA_RECOMMENDATION
1141                 else if (mediaControlKey.isSsReactivated) 43 // MEDIA_RESUME_SS_ACTIVATED
1142                 else 31, // MEDIA_RESUME
1143                 uid,
1144                 interactedSubcardRank,
1145                 interactedSubcardCardinality,
1146                 receivedLatencyMillis,
1147                 null, // Media cards cannot have subcards.
1148                 null // Media cards don't have dimensions today.
1149             )
1150             /* ktlint-disable max-line-length */
1151             if (DEBUG) {
1152                 Log.d(
1153                     TAG,
1154                     "Log Smartspace card event id: $eventId instance id: $instanceId" +
1155                         " surface: $surface rank: $rank cardinality: $cardinality " +
1156                         "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " +
1157                         "isSsReactivated: ${mediaControlKey.isSsReactivated}" +
1158                         "uid: $uid " +
1159                         "interactedSubcardRank: $interactedSubcardRank " +
1160                         "interactedSubcardCardinality: $interactedSubcardCardinality " +
1161                         "received_latency_millis: $receivedLatencyMillis"
1162                 )
1163             }
1164         }
1165     }
1166 
1167     private fun onSwipeToDismiss() {
1168         MediaPlayerData.players().forEachIndexed { index, it ->
1169             if (it.mIsImpressed) {
1170                 logSmartspaceCardReported(
1171                     SMARTSPACE_CARD_DISMISS_EVENT,
1172                     it.mSmartspaceId,
1173                     it.mUid,
1174                     intArrayOf(it.surfaceForSmartspaceLogging),
1175                     rank = index,
1176                     isSwipeToDismiss = true
1177                 )
1178                 // Reset card impressed state when swipe to dismissed
1179                 it.mIsImpressed = false
1180             }
1181         }
1182         logger.logSwipeDismiss()
1183         mediaManager.onSwipeToDismiss()
1184     }
1185 
1186     fun getCurrentVisibleMediaContentIntent(): PendingIntent? {
1187         return MediaPlayerData.playerKeys()
1188             .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
1189             ?.data
1190             ?.clickIntent
1191     }
1192 
1193     override fun dump(pw: PrintWriter, args: Array<out String>) {
1194         pw.apply {
1195             println("keysNeedRemoval: $keysNeedRemoval")
1196             println("dataKeys: ${MediaPlayerData.dataKeys()}")
1197             println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}")
1198             println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}")
1199             println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
1200             println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
1201             println("current size: $currentCarouselWidth x $currentCarouselHeight")
1202             println("location: $desiredLocation")
1203             println(
1204                 "state: ${desiredHostState?.expansion}, " +
1205                     "only active ${desiredHostState?.showsOnlyActiveMedia}"
1206             )
1207         }
1208     }
1209 }
1210 
1211 @VisibleForTesting
1212 internal object MediaPlayerData {
1213     private val EMPTY =
1214         MediaData(
1215             userId = -1,
1216             initialized = false,
1217             app = null,
1218             appIcon = null,
1219             artist = null,
1220             song = null,
1221             artwork = null,
1222             actions = emptyList(),
1223             actionsToShowInCompact = emptyList(),
1224             packageName = "INVALID",
1225             token = null,
1226             clickIntent = null,
1227             device = null,
1228             active = true,
1229             resumeAction = null,
1230             instanceId = InstanceId.fakeInstanceId(-1),
1231             appUid = -1
1232         )
1233     // Whether should prioritize Smartspace card.
1234     internal var shouldPrioritizeSs: Boolean = false
1235         private set
1236     internal var smartspaceMediaData: SmartspaceMediaData? = null
1237         private set
1238 
1239     data class MediaSortKey(
1240         val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation.
1241         val data: MediaData,
1242         val key: String,
1243         val updateTime: Long = 0,
1244         val isSsReactivated: Boolean = false
1245     )
1246 
1247     private val comparator =
<lambda>null1248         compareByDescending<MediaSortKey> {
1249                 it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL
1250             }
<lambda>null1251             .thenByDescending {
1252                 it.data.isPlaying == true &&
1253                     it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL
1254             }
<lambda>null1255             .thenByDescending { it.data.active }
<lambda>null1256             .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec }
<lambda>null1257             .thenByDescending { !it.data.resumption }
<lambda>null1258             .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
<lambda>null1259             .thenByDescending { it.data.lastActive }
<lambda>null1260             .thenByDescending { it.updateTime }
<lambda>null1261             .thenByDescending { it.data.notificationKey }
1262 
1263     private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
1264     private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
1265     // A map that tracks order of visible media players before they get reordered.
1266     private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>()
1267 
addMediaPlayernull1268     fun addMediaPlayer(
1269         key: String,
1270         data: MediaData,
1271         player: MediaControlPanel,
1272         clock: SystemClock,
1273         isSsReactivated: Boolean,
1274         debugLogger: MediaCarouselControllerLogger? = null
1275     ) {
1276         val removedPlayer = removeMediaPlayer(key)
1277         if (removedPlayer != null && removedPlayer != player) {
1278             debugLogger?.logPotentialMemoryLeak(key)
1279         }
1280         val sortKey =
1281             MediaSortKey(
1282                 isSsMediaRec = false,
1283                 data,
1284                 key,
1285                 clock.currentTimeMillis(),
1286                 isSsReactivated = isSsReactivated
1287             )
1288         mediaData.put(key, sortKey)
1289         mediaPlayers.put(sortKey, player)
1290         visibleMediaPlayers.put(key, sortKey)
1291     }
1292 
addMediaRecommendationnull1293     fun addMediaRecommendation(
1294         key: String,
1295         data: SmartspaceMediaData,
1296         player: MediaControlPanel,
1297         shouldPrioritize: Boolean,
1298         clock: SystemClock,
1299         debugLogger: MediaCarouselControllerLogger? = null,
1300         update: Boolean = false
1301     ) {
1302         shouldPrioritizeSs = shouldPrioritize
1303         val removedPlayer = removeMediaPlayer(key)
1304         if (!update && removedPlayer != null && removedPlayer != player) {
1305             debugLogger?.logPotentialMemoryLeak(key)
1306         }
1307         val sortKey =
1308             MediaSortKey(
1309                 isSsMediaRec = true,
1310                 EMPTY.copy(active = data.isActive, isPlaying = false),
1311                 key,
1312                 clock.currentTimeMillis(),
1313                 isSsReactivated = true
1314             )
1315         mediaData.put(key, sortKey)
1316         mediaPlayers.put(sortKey, player)
1317         visibleMediaPlayers.put(key, sortKey)
1318         smartspaceMediaData = data
1319     }
1320 
moveIfExistsnull1321     fun moveIfExists(
1322         oldKey: String?,
1323         newKey: String,
1324         debugLogger: MediaCarouselControllerLogger? = null
1325     ) {
1326         if (oldKey == null || oldKey == newKey) {
1327             return
1328         }
1329 
1330         mediaData.remove(oldKey)?.let {
1331             // MediaPlayer should not be visible
1332             // no need to set isDismissed flag.
1333             val removedPlayer = removeMediaPlayer(newKey)
1334             removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) }
1335             mediaData.put(newKey, it)
1336         }
1337     }
1338 
getMediaControlPanelnull1339     fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? {
1340         return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex))
1341     }
1342 
getMediaPlayernull1343     fun getMediaPlayer(key: String): MediaControlPanel? {
1344         return mediaData.get(key)?.let { mediaPlayers.get(it) }
1345     }
1346 
getMediaPlayerIndexnull1347     fun getMediaPlayerIndex(key: String): Int {
1348         val sortKey = mediaData.get(key)
1349         mediaPlayers.entries.forEachIndexed { index, e ->
1350             if (e.key == sortKey) {
1351                 return index
1352             }
1353         }
1354         return -1
1355     }
1356 
1357     /**
1358      * Removes media player given the key.
1359      *
1360      * @param isDismissed determines whether the media player is removed from the carousel.
1361      */
removeMediaPlayernull1362     fun removeMediaPlayer(key: String, isDismissed: Boolean = false) =
1363         mediaData.remove(key)?.let {
1364             if (it.isSsMediaRec) {
1365                 smartspaceMediaData = null
1366             }
1367             if (isDismissed) {
1368                 visibleMediaPlayers.remove(key)
1369             }
1370             mediaPlayers.remove(it)
1371         }
1372 
mediaDatanull1373     fun mediaData() =
1374         mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
1375 
dataKeysnull1376     fun dataKeys() = mediaData.keys
1377 
1378     fun players() = mediaPlayers.values
1379 
1380     fun playerKeys() = mediaPlayers.keys
1381 
1382     fun visiblePlayerKeys() = visibleMediaPlayers.values
1383 
1384     /** Returns the index of the first non-timeout media. */
1385     fun firstActiveMediaIndex(): Int {
1386         mediaPlayers.entries.forEachIndexed { index, e ->
1387             if (!e.key.isSsMediaRec && e.key.data.active) {
1388                 return index
1389             }
1390         }
1391         return -1
1392     }
1393 
1394     /** Returns the existing Smartspace target id. */
smartspaceMediaKeynull1395     fun smartspaceMediaKey(): String? {
1396         mediaData.entries.forEach { e ->
1397             if (e.value.isSsMediaRec) {
1398                 return e.key
1399             }
1400         }
1401         return null
1402     }
1403 
1404     @VisibleForTesting
clearnull1405     fun clear() {
1406         mediaData.clear()
1407         mediaPlayers.clear()
1408         visibleMediaPlayers.clear()
1409     }
1410 
1411     /* Returns true if there is active media player card or recommendation card */
hasActiveMediaOrRecommendationCardnull1412     fun hasActiveMediaOrRecommendationCard(): Boolean {
1413         if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) {
1414             return true
1415         }
1416         if (firstActiveMediaIndex() != -1) {
1417             return true
1418         }
1419         return false
1420     }
1421 
isSsReactivatednull1422     fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false
1423 
1424     /**
1425      * This method is called when media players are reordered. To make sure we have the new version
1426      * of the order of media players visible to user.
1427      */
1428     fun updateVisibleMediaPlayers() {
1429         visibleMediaPlayers.clear()
1430         playerKeys().forEach { visibleMediaPlayers.put(it.key, it) }
1431     }
1432 }
1433