• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 package com.android.systemui.media
2 
3 import android.content.Context
4 import android.content.Intent
5 import android.content.res.ColorStateList
6 import android.content.res.Configuration
7 import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
8 import android.util.Log
9 import android.util.MathUtils
10 import android.view.LayoutInflater
11 import android.view.View
12 import android.view.ViewGroup
13 import android.widget.LinearLayout
14 import androidx.annotation.VisibleForTesting
15 import com.android.systemui.Dumpable
16 import com.android.systemui.R
17 import com.android.systemui.classifier.FalsingCollector
18 import com.android.systemui.dagger.SysUISingleton
19 import com.android.systemui.dagger.qualifiers.Main
20 import com.android.systemui.dump.DumpManager
21 import com.android.systemui.plugins.ActivityStarter
22 import com.android.systemui.plugins.FalsingManager
23 import com.android.systemui.qs.PageIndicator
24 import com.android.systemui.shared.system.SysUiStatsLog
25 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager
26 import com.android.systemui.statusbar.policy.ConfigurationController
27 import com.android.systemui.util.Utils
28 import com.android.systemui.util.animation.UniqueObjectHostView
29 import com.android.systemui.util.animation.requiresRemeasuring
30 import com.android.systemui.util.concurrency.DelayableExecutor
31 import com.android.systemui.util.time.SystemClock
32 import java.io.FileDescriptor
33 import java.io.PrintWriter
34 import java.util.TreeMap
35 import javax.inject.Inject
36 import javax.inject.Provider
37 
38 private const val TAG = "MediaCarouselController"
39 private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
40 private const val DEBUG = false
41 
42 /**
43  * Class that is responsible for keeping the view carousel up to date.
44  * This also handles changes in state and applies them to the media carousel like the expansion.
45  */
46 @SysUISingleton
47 class MediaCarouselController @Inject constructor(
48     private val context: Context,
49     private val mediaControlPanelFactory: Provider<MediaControlPanel>,
50     private val visualStabilityManager: VisualStabilityManager,
51     private val mediaHostStatesManager: MediaHostStatesManager,
52     private val activityStarter: ActivityStarter,
53     private val systemClock: SystemClock,
54     @Main executor: DelayableExecutor,
55     private val mediaManager: MediaDataManager,
56     configurationController: ConfigurationController,
57     falsingCollector: FalsingCollector,
58     falsingManager: FalsingManager,
59     dumpManager: DumpManager
60 ) : Dumpable {
61     /**
62      * The current width of the carousel
63      */
64     private var currentCarouselWidth: Int = 0
65 
66     /**
67      * The current height of the carousel
68      */
69     private var currentCarouselHeight: Int = 0
70 
71     /**
72      * Are we currently showing only active players
73      */
74     private var currentlyShowingOnlyActive: Boolean = false
75 
76     /**
77      * Is the player currently visible (at the end of the transformation
78      */
79     private var playersVisible: Boolean = false
80     /**
81      * The desired location where we'll be at the end of the transformation. Usually this matches
82      * the end location, except when we're still waiting on a state update call.
83      */
84     @MediaLocation
85     private var desiredLocation: Int = -1
86 
87     /**
88      * The ending location of the view where it ends when all animations and transitions have
89      * finished
90      */
91     @MediaLocation
92     private var currentEndLocation: Int = -1
93 
94     /**
95      * The ending location of the view where it ends when all animations and transitions have
96      * finished
97      */
98     @MediaLocation
99     private var currentStartLocation: Int = -1
100 
101     /**
102      * The progress of the transition or 1.0 if there is no transition happening
103      */
104     private var currentTransitionProgress: Float = 1.0f
105 
106     /**
107      * The measured width of the carousel
108      */
109     private var carouselMeasureWidth: Int = 0
110 
111     /**
112      * The measured height of the carousel
113      */
114     private var carouselMeasureHeight: Int = 0
115     private var desiredHostState: MediaHostState? = null
116     private val mediaCarousel: MediaScrollView
117     val mediaCarouselScrollHandler: MediaCarouselScrollHandler
118     val mediaFrame: ViewGroup
119     private lateinit var settingsButton: View
120     private val mediaContent: ViewGroup
121     private val pageIndicator: PageIndicator
122     private val visualStabilityCallback: VisualStabilityManager.Callback
123     private var needsReordering: Boolean = false
124     private var keysNeedRemoval = mutableSetOf<String>()
125     private var bgColor = getBackgroundColor()
126     protected var shouldScrollToActivePlayer: Boolean = false
127     private var isRtl: Boolean = false
128         set(value) {
129             if (value != field) {
130                 field = value
131                 mediaFrame.layoutDirection =
132                         if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
133                 mediaCarouselScrollHandler.scrollToStart()
134             }
135         }
136     private var currentlyExpanded = true
137         set(value) {
138             if (field != value) {
139                 field = value
140                 for (player in MediaPlayerData.players()) {
141                     player.setListening(field)
142                 }
143             }
144         }
145     private val configListener = object : ConfigurationController.ConfigurationListener {
146         override fun onDensityOrFontScaleChanged() {
147             recreatePlayers()
148             inflateSettingsButton()
149         }
150 
151         override fun onOverlayChanged() {
152             recreatePlayers()
153             inflateSettingsButton()
154         }
155 
156         override fun onConfigChanged(newConfig: Configuration?) {
157             if (newConfig == null) return
158             isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
159         }
160 
161         override fun onUiModeChanged() {
162             recreatePlayers()
163             inflateSettingsButton()
164         }
165     }
166 
167     /**
168      * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility.
169      * It will be called when the container is out of view.
170      */
171     lateinit var updateUserVisibility: () -> Unit
172 
173     init {
174         dumpManager.registerDumpable(TAG, this)
175         mediaFrame = inflateMediaCarousel()
176         mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
177         pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
178         mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
179                 executor, this::onSwipeToDismiss, this::updatePageIndicatorLocation,
180                 this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression)
181         isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
182         inflateSettingsButton()
183         mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
184         configurationController.addCallback(configListener)
185         // TODO (b/162832756): remove visual stability manager when migrating to new pipeline
186         visualStabilityCallback = VisualStabilityManager.Callback {
187             if (needsReordering) {
188                 needsReordering = false
189                 reorderAllPlayers(previousVisiblePlayerKey = null)
190             }
191 
192             keysNeedRemoval.forEach { removePlayer(it) }
193             keysNeedRemoval.clear()
194 
195             // Update user visibility so that no extra impression will be logged when
196             // activeMediaIndex resets to 0
197             if (this::updateUserVisibility.isInitialized) {
198                 updateUserVisibility()
199             }
200 
201             // Let's reset our scroll position
202             mediaCarouselScrollHandler.scrollToStart()
203         }
204         visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback,
205                 true /* persistent */)
206         mediaManager.addListener(object : MediaDataManager.Listener {
207             override fun onMediaDataLoaded(
208                 key: String,
209                 oldKey: String?,
210                 data: MediaData,
211                 immediately: Boolean,
212                 isSsReactivated: Boolean
213             ) {
214                 if (addOrUpdatePlayer(key, oldKey, data)) {
215                     MediaPlayerData.getMediaPlayer(key)?.let {
216                         logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
217                                 it.mInstanceId,
218                                 /* isRecommendationCard */ false,
219                                 it.surfaceForSmartspaceLogging,
220                                 rank = MediaPlayerData.getMediaPlayerIndex(key))
221                     }
222                 }
223                 if (mediaCarouselScrollHandler.visibleToUser &&
224                         isSsReactivated && !mediaCarouselScrollHandler.qsExpanded) {
225                     // It could happen that reactived media player isn't visible to user because
226                     // of it is a resumption card.
227                     logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
228                 }
229                 val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
230                 if (canRemove && !Utils.useMediaResumption(context)) {
231                     // This view isn't playing, let's remove this! This happens e.g when
232                     // dismissing/timing out a view. We still have the data around because
233                     // resumption could be on, but we should save the resources and release this.
234                     if (visualStabilityManager.isReorderingAllowed) {
235                         onMediaDataRemoved(key)
236                     } else {
237                         keysNeedRemoval.add(key)
238                     }
239                 } else {
240                     keysNeedRemoval.remove(key)
241                 }
242             }
243 
244             override fun onSmartspaceMediaDataLoaded(
245                 key: String,
246                 data: SmartspaceMediaData,
247                 shouldPrioritize: Boolean
248             ) {
249                 if (DEBUG) Log.d(TAG, "Loading Smartspace media update")
250                 if (data.isActive) {
251                     addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
252                     MediaPlayerData.getMediaPlayer(key)?.let {
253                         logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
254                                 it.mInstanceId,
255                                 /* isRecommendationCard */ true,
256                                 it.surfaceForSmartspaceLogging,
257                                 rank = MediaPlayerData.getMediaPlayerIndex(key))
258 
259                         if (mediaCarouselScrollHandler.visibleToUser &&
260                                 mediaCarouselScrollHandler.visibleMediaIndex ==
261                                 MediaPlayerData.getMediaPlayerIndex(key)) {
262                             logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN
263                                     it.mInstanceId,
264                                     /* isRecommendationCard */ true,
265                                     it.surfaceForSmartspaceLogging)
266                         }
267                     }
268                 } else {
269                     onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
270                 }
271             }
272 
273             override fun onMediaDataRemoved(key: String) {
274                 removePlayer(key)
275             }
276 
277             override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
278                 if (DEBUG) Log.d(TAG, "My Smartspace media removal request is received")
279                 if (immediately || visualStabilityManager.isReorderingAllowed) {
280                     onMediaDataRemoved(key)
281                 } else {
282                     keysNeedRemoval.add(key)
283                 }
284             }
285         })
286         mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
287             // The pageIndicator is not laid out yet when we get the current state update,
288             // Lets make sure we have the right dimensions
289             updatePageIndicatorLocation()
290         }
291         mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
292             override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
293                 if (location == desiredLocation) {
294                     onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
295                 }
296             }
297         })
298     }
299 
300     private fun inflateSettingsButton() {
301         val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button,
302                 mediaFrame, false) as View
303         if (this::settingsButton.isInitialized) {
304             mediaFrame.removeView(settingsButton)
305         }
306         settingsButton = settings
307         mediaFrame.addView(settingsButton)
308         mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
309         settingsButton.setOnClickListener {
310             activityStarter.startActivity(settingsIntent, true /* dismissShade */)
311         }
312     }
313 
314     private fun inflateMediaCarousel(): ViewGroup {
315         val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel,
316                 UniqueObjectHostView(context), false) as ViewGroup
317         // Because this is inflated when not attached to the true view hierarchy, it resolves some
318         // potential issues to force that the layout direction is defined by the locale
319         // (rather than inherited from the parent, which would resolve to LTR when unattached).
320         mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
321         return mediaCarousel
322     }
323 
324     private fun reorderAllPlayers(previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?) {
325         mediaContent.removeAllViews()
326         for (mediaPlayer in MediaPlayerData.players()) {
327             mediaPlayer.playerViewHolder?.let {
328                 mediaContent.addView(it.player)
329             } ?: mediaPlayer.recommendationViewHolder?.let {
330                 mediaContent.addView(it.recommendations)
331             }
332         }
333         mediaCarouselScrollHandler.onPlayersChanged()
334 
335         // Automatically scroll to the active player if needed
336         if (shouldScrollToActivePlayer) {
337             shouldScrollToActivePlayer = false
338             val activeMediaIndex = MediaPlayerData.firstActiveMediaIndex()
339             if (activeMediaIndex != -1) {
340                 previousVisiblePlayerKey?.let {
341                     val previousVisibleIndex = MediaPlayerData.playerKeys()
342                         .indexOfFirst { key -> it == key }
343                     mediaCarouselScrollHandler
344                         .scrollToPlayer(previousVisibleIndex, activeMediaIndex)
345                 } ?: {
346                     mediaCarouselScrollHandler.scrollToPlayer(destIndex = activeMediaIndex)
347                 }
348             }
349         }
350     }
351 
352     // Returns true if new player is added
353     private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData): Boolean {
354         val dataCopy = data.copy(backgroundColor = bgColor)
355         MediaPlayerData.moveIfExists(oldKey, key)
356         val existingPlayer = MediaPlayerData.getMediaPlayer(key)
357         val curVisibleMediaKey = MediaPlayerData.playerKeys()
358             .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
359         if (existingPlayer == null) {
360             var newPlayer = mediaControlPanelFactory.get()
361             newPlayer.attachPlayer(
362                 PlayerViewHolder.create(LayoutInflater.from(context), mediaContent))
363             newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
364             val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
365                     ViewGroup.LayoutParams.WRAP_CONTENT)
366             newPlayer.playerViewHolder?.player?.setLayoutParams(lp)
367             newPlayer.bindPlayer(dataCopy, key)
368             newPlayer.setListening(currentlyExpanded)
369             MediaPlayerData.addMediaPlayer(key, dataCopy, newPlayer, systemClock)
370             updatePlayerToState(newPlayer, noAnimation = true)
371             reorderAllPlayers(curVisibleMediaKey)
372         } else {
373             existingPlayer.bindPlayer(dataCopy, key)
374             MediaPlayerData.addMediaPlayer(key, dataCopy, existingPlayer, systemClock)
375             if (visualStabilityManager.isReorderingAllowed || shouldScrollToActivePlayer) {
376                 reorderAllPlayers(curVisibleMediaKey)
377             } else {
378                 needsReordering = true
379             }
380         }
381         updatePageIndicator()
382         mediaCarouselScrollHandler.onPlayersChanged()
383         mediaCarousel.requiresRemeasuring = true
384         // Check postcondition: mediaContent should have the same number of children as there are
385         // elements in mediaPlayers.
386         if (MediaPlayerData.players().size != mediaContent.childCount) {
387             Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
388         }
389         return existingPlayer == null
390     }
391 
392     private fun addSmartspaceMediaRecommendations(
393         key: String,
394         data: SmartspaceMediaData,
395         shouldPrioritize: Boolean
396     ) {
397         if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
398         if (MediaPlayerData.getMediaPlayer(key) != null) {
399             Log.w(TAG, "Skip adding smartspace target in carousel")
400             return
401         }
402 
403         val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
404         existingSmartspaceMediaKey?.let {
405             MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey)
406         }
407 
408         var newRecs = mediaControlPanelFactory.get()
409         newRecs.attachRecommendation(
410             RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent))
411         newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
412         val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
413             ViewGroup.LayoutParams.WRAP_CONTENT)
414         newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
415         newRecs.bindRecommendation(data.copy(backgroundColor = bgColor))
416         val curVisibleMediaKey = MediaPlayerData.playerKeys()
417             .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
418         MediaPlayerData.addMediaRecommendation(key, data, newRecs, shouldPrioritize, systemClock)
419         updatePlayerToState(newRecs, noAnimation = true)
420         reorderAllPlayers(curVisibleMediaKey)
421         updatePageIndicator()
422         mediaCarousel.requiresRemeasuring = true
423         // Check postcondition: mediaContent should have the same number of children as there are
424         // elements in mediaPlayers.
425         if (MediaPlayerData.players().size != mediaContent.childCount) {
426             Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
427         }
428     }
429 
430     fun removePlayer(
431         key: String,
432         dismissMediaData: Boolean = true,
433         dismissRecommendation: Boolean = true
434     ) {
435         val removed = MediaPlayerData.removeMediaPlayer(key)
436         removed?.apply {
437             mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
438             mediaContent.removeView(removed.playerViewHolder?.player)
439             mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
440             removed.onDestroy()
441             mediaCarouselScrollHandler.onPlayersChanged()
442             updatePageIndicator()
443 
444             if (dismissMediaData) {
445                 // Inform the media manager of a potentially late dismissal
446                 mediaManager.dismissMediaData(key, delay = 0L)
447             }
448             if (dismissRecommendation) {
449                 // Inform the media manager of a potentially late dismissal
450                 mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
451             }
452         }
453     }
454 
455     private fun recreatePlayers() {
456         bgColor = getBackgroundColor()
457         pageIndicator.tintList = ColorStateList.valueOf(getForegroundColor())
458 
459         MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
460             if (isSsMediaRec) {
461                 val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
462                 removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
463                 smartspaceMediaData?.let {
464                     addSmartspaceMediaRecommendations(
465                         it.targetId, it, MediaPlayerData.shouldPrioritizeSs)
466                 }
467             } else {
468                 removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
469                 addOrUpdatePlayer(key = key, oldKey = null, data = data)
470             }
471         }
472     }
473 
474     private fun getBackgroundColor(): Int {
475         return context.getColor(android.R.color.system_accent2_50)
476     }
477 
478     private fun getForegroundColor(): Int {
479         return context.getColor(android.R.color.system_accent2_900)
480     }
481 
482     private fun updatePageIndicator() {
483         val numPages = mediaContent.getChildCount()
484         pageIndicator.setNumPages(numPages)
485         if (numPages == 1) {
486             pageIndicator.setLocation(0f)
487         }
488         updatePageIndicatorAlpha()
489     }
490 
491     /**
492      * Set a new interpolated state for all players. This is a state that is usually controlled
493      * by a finger movement where the user drags from one state to the next.
494      *
495      * @param startLocation the start location of our state or -1 if this is directly set
496      * @param endLocation the ending location of our state.
497      * @param progress the progress of the transition between startLocation and endlocation. If
498      *                 this is not a guided transformation, this will be 1.0f
499      * @param immediately should this state be applied immediately, canceling all animations?
500      */
501     fun setCurrentState(
502         @MediaLocation startLocation: Int,
503         @MediaLocation endLocation: Int,
504         progress: Float,
505         immediately: Boolean
506     ) {
507         if (startLocation != currentStartLocation ||
508                 endLocation != currentEndLocation ||
509                 progress != currentTransitionProgress ||
510                 immediately
511         ) {
512             currentStartLocation = startLocation
513             currentEndLocation = endLocation
514             currentTransitionProgress = progress
515             for (mediaPlayer in MediaPlayerData.players()) {
516                 updatePlayerToState(mediaPlayer, immediately)
517             }
518             maybeResetSettingsCog()
519             updatePageIndicatorAlpha()
520         }
521     }
522 
523     private fun updatePageIndicatorAlpha() {
524         val hostStates = mediaHostStatesManager.mediaHostStates
525         val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
526         val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
527         val startAlpha = if (startIsVisible) 1.0f else 0.0f
528         val endAlpha = if (endIsVisible) 1.0f else 0.0f
529         var alpha = 1.0f
530         if (!endIsVisible || !startIsVisible) {
531             var progress = currentTransitionProgress
532             if (!endIsVisible) {
533                 progress = 1.0f - progress
534             }
535             // Let's fade in quickly at the end where the view is visible
536             progress = MathUtils.constrain(
537                     MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress),
538                     0.0f,
539                     1.0f)
540             alpha = MathUtils.lerp(startAlpha, endAlpha, progress)
541         }
542         pageIndicator.alpha = alpha
543     }
544 
545     private fun updatePageIndicatorLocation() {
546         // Update the location of the page indicator, carousel clipping
547         val translationX = if (isRtl) {
548             (pageIndicator.width - currentCarouselWidth) / 2.0f
549         } else {
550             (currentCarouselWidth - pageIndicator.width) / 2.0f
551         }
552         pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
553         val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
554         pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height -
555                 layoutParams.bottomMargin).toFloat()
556     }
557 
558     /**
559      * Update the dimension of this carousel.
560      */
561     private fun updateCarouselDimensions() {
562         var width = 0
563         var height = 0
564         for (mediaPlayer in MediaPlayerData.players()) {
565             val controller = mediaPlayer.mediaViewController
566             // When transitioning the view to gone, the view gets smaller, but the translation
567             // Doesn't, let's add the translation
568             width = Math.max(width, controller.currentWidth + controller.translationX.toInt())
569             height = Math.max(height, controller.currentHeight + controller.translationY.toInt())
570         }
571         if (width != currentCarouselWidth || height != currentCarouselHeight) {
572             currentCarouselWidth = width
573             currentCarouselHeight = height
574             mediaCarouselScrollHandler.setCarouselBounds(
575                     currentCarouselWidth, currentCarouselHeight)
576             updatePageIndicatorLocation()
577         }
578     }
579 
580     private fun maybeResetSettingsCog() {
581         val hostStates = mediaHostStatesManager.mediaHostStates
582         val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia
583                 ?: true
584         val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia
585                 ?: endShowsActive
586         if (currentlyShowingOnlyActive != endShowsActive ||
587                 ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
588                             startShowsActive != endShowsActive)) {
589             // Whenever we're transitioning from between differing states or the endstate differs
590             // we reset the translation
591             currentlyShowingOnlyActive = endShowsActive
592             mediaCarouselScrollHandler.resetTranslation(animate = true)
593         }
594     }
595 
596     private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
597         mediaPlayer.mediaViewController.setCurrentState(
598                 startLocation = currentStartLocation,
599                 endLocation = currentEndLocation,
600                 transitionProgress = currentTransitionProgress,
601                 applyImmediately = noAnimation)
602     }
603 
604     /**
605      * The desired location of this view has changed. We should remeasure the view to match
606      * the new bounds and kick off bounds animations if necessary.
607      * If an animation is happening, an animation is kicked of externally, which sets a new
608      * current state until we reach the targetState.
609      *
610      * @param desiredLocation the location we're going to
611      * @param desiredHostState the target state we're transitioning to
612      * @param animate should this be animated
613      */
614     fun onDesiredLocationChanged(
615         desiredLocation: Int,
616         desiredHostState: MediaHostState?,
617         animate: Boolean,
618         duration: Long = 200,
619         startDelay: Long = 0
620     ) {
621         desiredHostState?.let {
622             // This is a hosting view, let's remeasure our players
623             this.desiredLocation = desiredLocation
624             this.desiredHostState = it
625             currentlyExpanded = it.expansion > 0
626 
627             val shouldCloseGuts = !currentlyExpanded && !mediaManager.hasActiveMedia() &&
628                     desiredHostState.showsOnlyActiveMedia
629 
630             for (mediaPlayer in MediaPlayerData.players()) {
631                 if (animate) {
632                     mediaPlayer.mediaViewController.animatePendingStateChange(
633                             duration = duration,
634                             delay = startDelay)
635                 }
636                 if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) {
637                     mediaPlayer.closeGuts(!animate)
638                 }
639 
640                 mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
641             }
642             mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
643             mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
644             val nowVisible = it.visible
645             if (nowVisible != playersVisible) {
646                 playersVisible = nowVisible
647                 if (nowVisible) {
648                     mediaCarouselScrollHandler.resetTranslation()
649                 }
650             }
651             updateCarouselSize()
652         }
653     }
654 
655     fun closeGuts(immediate: Boolean = true) {
656         MediaPlayerData.players().forEach {
657             it.closeGuts(immediate)
658         }
659     }
660 
661     /**
662      * Update the size of the carousel, remeasuring it if necessary.
663      */
664     private fun updateCarouselSize() {
665         val width = desiredHostState?.measurementInput?.width ?: 0
666         val height = desiredHostState?.measurementInput?.height ?: 0
667         if (width != carouselMeasureWidth && width != 0 ||
668                 height != carouselMeasureHeight && height != 0) {
669             carouselMeasureWidth = width
670             carouselMeasureHeight = height
671             val playerWidthPlusPadding = carouselMeasureWidth +
672                     context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
673             // Let's remeasure the carousel
674             val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
675             val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
676             mediaCarousel.measure(widthSpec, heightSpec)
677             mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
678             // Update the padding after layout; view widths are used in RTL to calculate scrollX
679             mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
680         }
681     }
682 
683     /**
684      * Log the user impression for media card at visibleMediaIndex.
685      */
686     fun logSmartspaceImpression(qsExpanded: Boolean) {
687         val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
688         if (MediaPlayerData.players().size > visibleMediaIndex) {
689             val mediaControlPanel = MediaPlayerData.players().elementAt(visibleMediaIndex)
690             val hasActiveMediaOrRecommendationCard =
691                     MediaPlayerData.hasActiveMediaOrRecommendationCard()
692             val isRecommendationCard = mediaControlPanel.recommendationViewHolder != null
693             if (!hasActiveMediaOrRecommendationCard && !qsExpanded) {
694                 // Skip logging if on LS or QQS, and there is no active media card
695                 return
696             }
697             logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN
698                     mediaControlPanel.mInstanceId,
699                     isRecommendationCard,
700                     mediaControlPanel.surfaceForSmartspaceLogging)
701         }
702     }
703 
704     @JvmOverloads
705     fun logSmartspaceCardReported(
706         eventId: Int,
707         instanceId: Int,
708         isRecommendationCard: Boolean,
709         surface: Int,
710         rank: Int = mediaCarouselScrollHandler.visibleMediaIndex
711     ) {
712         // Only log media resume card when Smartspace data is available
713         if (!isRecommendationCard &&
714                         !mediaManager.smartspaceMediaData.isActive &&
715                                 MediaPlayerData.smartspaceMediaData == null) {
716             return
717         }
718 
719         /* ktlint-disable max-line-length */
720         SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
721                 eventId,
722                 instanceId,
723                 if (isRecommendationCard)
724                     SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__HEADPHONE_MEDIA_RECOMMENDATIONS
725                 else
726                     SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__HEADPHONE_RESUME_MEDIA,
727                 surface,
728                 rank,
729                 mediaContent.getChildCount())
730         /* ktlint-disable max-line-length */
731     }
732 
733     private fun onSwipeToDismiss() {
734         val recommendation = MediaPlayerData.players().filter {
735             it.recommendationViewHolder != null
736         }
737         // Use -1 as rank value to indicate user swipe to dismiss the card
738         if (!recommendation.isEmpty()) {
739             logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
740                     recommendation.get(0).mInstanceId,
741                     true,
742                     recommendation.get(0).surfaceForSmartspaceLogging,
743             /* rank */-1)
744         } else {
745             val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
746             if (MediaPlayerData.players().size > visibleMediaIndex) {
747                 val player = MediaPlayerData.players().elementAt(visibleMediaIndex)
748                 logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
749                         player.mInstanceId,
750                 false,
751                         player.surfaceForSmartspaceLogging,
752                 /* rank */-1)
753             }
754         }
755         mediaManager.onSwipeToDismiss()
756     }
757 
758     override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
759         pw.apply {
760             println("keysNeedRemoval: $keysNeedRemoval")
761             println("playerKeys: ${MediaPlayerData.playerKeys()}")
762             println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
763             println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
764         }
765     }
766 }
767 
768 @VisibleForTesting
769 internal object MediaPlayerData {
770     private val EMPTY = MediaData(-1, false, 0, null, null, null, null, null,
771         emptyList(), emptyList(), "INVALID", null, null, null, true, null)
772     // Whether should prioritize Smartspace card.
773     internal var shouldPrioritizeSs: Boolean = false
774         private set
775     internal var smartspaceMediaData: SmartspaceMediaData? = null
776         private set
777 
778     data class MediaSortKey(
779         // Whether the item represents a Smartspace media recommendation.
780         val isSsMediaRec: Boolean,
781         val data: MediaData,
782         val updateTime: Long = 0
783     )
784 
785     private val comparator =
<lambda>null786         compareByDescending<MediaSortKey> { it.data.isPlaying }
<lambda>null787             .thenByDescending { if (shouldPrioritizeSs) it.isSsMediaRec else !it.isSsMediaRec }
<lambda>null788             .thenByDescending { it.data.isLocalSession }
<lambda>null789             .thenByDescending { !it.data.resumption }
<lambda>null790             .thenByDescending { it.updateTime }
791 
792     private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
793     private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
794 
addMediaPlayernull795     fun addMediaPlayer(key: String, data: MediaData, player: MediaControlPanel, clock: SystemClock) {
796         removeMediaPlayer(key)
797         val sortKey = MediaSortKey(isSsMediaRec = false, data, clock.currentTimeMillis())
798         mediaData.put(key, sortKey)
799         mediaPlayers.put(sortKey, player)
800     }
801 
addMediaRecommendationnull802     fun addMediaRecommendation(
803         key: String,
804         data: SmartspaceMediaData,
805         player: MediaControlPanel,
806         shouldPrioritize: Boolean,
807         clock: SystemClock
808     ) {
809         shouldPrioritizeSs = shouldPrioritize
810         removeMediaPlayer(key)
811         val sortKey = MediaSortKey(isSsMediaRec = true, EMPTY, clock.currentTimeMillis())
812         mediaData.put(key, sortKey)
813         mediaPlayers.put(sortKey, player)
814         smartspaceMediaData = data
815     }
816 
moveIfExistsnull817     fun moveIfExists(oldKey: String?, newKey: String) {
818         if (oldKey == null || oldKey == newKey) {
819             return
820         }
821 
822         mediaData.remove(oldKey)?.let {
823             removeMediaPlayer(newKey)
824             mediaData.put(newKey, it)
825         }
826     }
827 
getMediaPlayernull828     fun getMediaPlayer(key: String): MediaControlPanel? {
829         return mediaData.get(key)?.let { mediaPlayers.get(it) }
830     }
831 
getMediaPlayerIndexnull832     fun getMediaPlayerIndex(key: String): Int {
833         val sortKey = mediaData.get(key)
834         mediaPlayers.entries.forEachIndexed { index, e ->
835             if (e.key == sortKey) {
836                 return index
837             }
838         }
839         return -1
840     }
841 
<lambda>null842     fun removeMediaPlayer(key: String) = mediaData.remove(key)?.let {
843         if (it.isSsMediaRec) {
844             smartspaceMediaData = null
845         }
846         mediaPlayers.remove(it)
847     }
848 
mediaDatanull849     fun mediaData() = mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
850 
playersnull851     fun players() = mediaPlayers.values
852 
853     fun playerKeys() = mediaPlayers.keys
854 
855     /** Returns the index of the first non-timeout media. */
856     fun firstActiveMediaIndex(): Int {
857         mediaPlayers.entries.forEachIndexed { index, e ->
858             if (!e.key.isSsMediaRec && e.key.data.active) {
859                 return index
860             }
861         }
862         return -1
863     }
864 
865     /** Returns the existing Smartspace target id. */
smartspaceMediaKeynull866     fun smartspaceMediaKey(): String? {
867         mediaData.entries.forEach { e ->
868             if (e.value.isSsMediaRec) {
869                 return e.key
870             }
871         }
872         return null
873     }
874 
875     @VisibleForTesting
clearnull876     fun clear() {
877         mediaData.clear()
878         mediaPlayers.clear()
879     }
880 
881     /* Returns true if there is active media player card or recommendation card */
hasActiveMediaOrRecommendationCardnull882     fun hasActiveMediaOrRecommendationCard(): Boolean {
883         if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) {
884             return true
885         }
886         if (firstActiveMediaIndex() != -1) {
887             return true
888         }
889         return false
890     }
891 }
892