<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