• 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.Configuration
6 import android.graphics.Color
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.R
16 import com.android.systemui.dagger.qualifiers.Main
17 import com.android.systemui.plugins.ActivityStarter
18 import com.android.systemui.plugins.FalsingManager
19 import com.android.systemui.qs.PageIndicator
20 import com.android.systemui.statusbar.notification.VisualStabilityManager
21 import com.android.systemui.statusbar.policy.ConfigurationController
22 import com.android.systemui.util.Utils
23 import com.android.systemui.util.animation.UniqueObjectHostView
24 import com.android.systemui.util.animation.requiresRemeasuring
25 import com.android.systemui.util.concurrency.DelayableExecutor
26 import java.util.TreeMap
27 import javax.inject.Inject
28 import javax.inject.Provider
29 import javax.inject.Singleton
30 
31 private const val TAG = "MediaCarouselController"
32 private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
33 
34 /**
35  * Class that is responsible for keeping the view carousel up to date.
36  * This also handles changes in state and applies them to the media carousel like the expansion.
37  */
38 @Singleton
39 class MediaCarouselController @Inject constructor(
40     private val context: Context,
41     private val mediaControlPanelFactory: Provider<MediaControlPanel>,
42     private val visualStabilityManager: VisualStabilityManager,
43     private val mediaHostStatesManager: MediaHostStatesManager,
44     private val activityStarter: ActivityStarter,
45     @Main executor: DelayableExecutor,
46     private val mediaManager: MediaDataManager,
47     configurationController: ConfigurationController,
48     falsingManager: FalsingManager
49 ) {
50     /**
51      * The current width of the carousel
52      */
53     private var currentCarouselWidth: Int = 0
54 
55     /**
56      * The current height of the carousel
57      */
58     private var currentCarouselHeight: Int = 0
59 
60     /**
61      * Are we currently showing only active players
62      */
63     private var currentlyShowingOnlyActive: Boolean = false
64 
65     /**
66      * Is the player currently visible (at the end of the transformation
67      */
68     private var playersVisible: Boolean = false
69     /**
70      * The desired location where we'll be at the end of the transformation. Usually this matches
71      * the end location, except when we're still waiting on a state update call.
72      */
73     @MediaLocation
74     private var desiredLocation: Int = -1
75 
76     /**
77      * The ending location of the view where it ends when all animations and transitions have
78      * finished
79      */
80     @MediaLocation
81     private var currentEndLocation: Int = -1
82 
83     /**
84      * The ending location of the view where it ends when all animations and transitions have
85      * finished
86      */
87     @MediaLocation
88     private var currentStartLocation: Int = -1
89 
90     /**
91      * The progress of the transition or 1.0 if there is no transition happening
92      */
93     private var currentTransitionProgress: Float = 1.0f
94 
95     /**
96      * The measured width of the carousel
97      */
98     private var carouselMeasureWidth: Int = 0
99 
100     /**
101      * The measured height of the carousel
102      */
103     private var carouselMeasureHeight: Int = 0
104     private var desiredHostState: MediaHostState? = null
105     private val mediaCarousel: MediaScrollView
106     private val mediaCarouselScrollHandler: MediaCarouselScrollHandler
107     val mediaFrame: ViewGroup
108     private lateinit var settingsButton: View
109     private val mediaContent: ViewGroup
110     private val pageIndicator: PageIndicator
111     private val visualStabilityCallback: VisualStabilityManager.Callback
112     private var needsReordering: Boolean = false
113     private var keysNeedRemoval = mutableSetOf<String>()
114     private var isRtl: Boolean = false
115         set(value) {
116             if (value != field) {
117                 field = value
118                 mediaFrame.layoutDirection =
119                         if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
120                 mediaCarouselScrollHandler.scrollToStart()
121             }
122         }
123     private var currentlyExpanded = true
124         set(value) {
125             if (field != value) {
126                 field = value
127                 for (player in MediaPlayerData.players()) {
128                     player.setListening(field)
129                 }
130             }
131         }
132     private val configListener = object : ConfigurationController.ConfigurationListener {
133         override fun onDensityOrFontScaleChanged() {
134             recreatePlayers()
135             inflateSettingsButton()
136         }
137 
138         override fun onOverlayChanged() {
139             recreatePlayers()
140             inflateSettingsButton()
141         }
142 
143         override fun onConfigChanged(newConfig: Configuration?) {
144             if (newConfig == null) return
145             isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
146         }
147     }
148 
149     init {
150         mediaFrame = inflateMediaCarousel()
151         mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
152         pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
153         mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
154                 executor, mediaManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
155                 this::closeGuts, falsingManager)
156         isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
157         inflateSettingsButton()
158         mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
159         configurationController.addCallback(configListener)
160         visualStabilityCallback = VisualStabilityManager.Callback {
161             if (needsReordering) {
162                 needsReordering = false
163                 reorderAllPlayers()
164             }
165 
166             keysNeedRemoval.forEach { removePlayer(it) }
167             keysNeedRemoval.clear()
168 
169             // Let's reset our scroll position
170             mediaCarouselScrollHandler.scrollToStart()
171         }
172         visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback,
173                 true /* persistent */)
174         mediaManager.addListener(object : MediaDataManager.Listener {
175             override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
176                 addOrUpdatePlayer(key, oldKey, data)
177                 val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
178                 if (canRemove && !Utils.useMediaResumption(context)) {
179                     // This view isn't playing, let's remove this! This happens e.g when
180                     // dismissing/timing out a view. We still have the data around because
181                     // resumption could be on, but we should save the resources and release this.
182                     if (visualStabilityManager.isReorderingAllowed) {
183                         onMediaDataRemoved(key)
184                     } else {
185                         keysNeedRemoval.add(key)
186                     }
187                 } else {
188                     keysNeedRemoval.remove(key)
189                 }
190             }
191 
192             override fun onMediaDataRemoved(key: String) {
193                 removePlayer(key)
194             }
195         })
196         mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
197             // The pageIndicator is not laid out yet when we get the current state update,
198             // Lets make sure we have the right dimensions
199             updatePageIndicatorLocation()
200         }
201         mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
202             override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
203                 if (location == desiredLocation) {
204                     onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
205                 }
206             }
207         })
208     }
209 
210     private fun inflateSettingsButton() {
211         val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button,
212                 mediaFrame, false) as View
213         if (this::settingsButton.isInitialized) {
214             mediaFrame.removeView(settingsButton)
215         }
216         settingsButton = settings
217         mediaFrame.addView(settingsButton)
218         mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
219         settingsButton.setOnClickListener {
220             activityStarter.startActivity(settingsIntent, true /* dismissShade */)
221         }
222     }
223 
224     private fun inflateMediaCarousel(): ViewGroup {
225         val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel,
226                 UniqueObjectHostView(context), false) as ViewGroup
227         // Because this is inflated when not attached to the true view hierarchy, it resolves some
228         // potential issues to force that the layout direction is defined by the locale
229         // (rather than inherited from the parent, which would resolve to LTR when unattached).
230         mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
231         return mediaCarousel
232     }
233 
234     private fun reorderAllPlayers() {
235         mediaContent.removeAllViews()
236         for (mediaPlayer in MediaPlayerData.players()) {
237             mediaPlayer.view?.let {
238                 mediaContent.addView(it.player)
239             }
240         }
241         mediaCarouselScrollHandler.onPlayersChanged()
242     }
243 
244     private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) {
245         val existingPlayer = MediaPlayerData.getMediaPlayer(key, oldKey)
246         if (existingPlayer == null) {
247             var newPlayer = mediaControlPanelFactory.get()
248             newPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), mediaContent))
249             newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
250             val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
251                     ViewGroup.LayoutParams.WRAP_CONTENT)
252             newPlayer.view?.player?.setLayoutParams(lp)
253             newPlayer.bind(data, key)
254             newPlayer.setListening(currentlyExpanded)
255             MediaPlayerData.addMediaPlayer(key, data, newPlayer)
256             updatePlayerToState(newPlayer, noAnimation = true)
257             reorderAllPlayers()
258         } else {
259             existingPlayer.bind(data, key)
260             MediaPlayerData.addMediaPlayer(key, data, existingPlayer)
261             if (visualStabilityManager.isReorderingAllowed) {
262                 reorderAllPlayers()
263             } else {
264                 needsReordering = true
265             }
266         }
267         updatePageIndicator()
268         mediaCarouselScrollHandler.onPlayersChanged()
269         mediaCarousel.requiresRemeasuring = true
270         // Check postcondition: mediaContent should have the same number of children as there are
271         // elements in mediaPlayers.
272         if (MediaPlayerData.players().size != mediaContent.childCount) {
273             Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
274         }
275     }
276 
277     private fun removePlayer(key: String, dismissMediaData: Boolean = true) {
278         val removed = MediaPlayerData.removeMediaPlayer(key)
279         removed?.apply {
280             mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
281             mediaContent.removeView(removed.view?.player)
282             removed.onDestroy()
283             mediaCarouselScrollHandler.onPlayersChanged()
284             updatePageIndicator()
285 
286             if (dismissMediaData) {
287                 // Inform the media manager of a potentially late dismissal
288                 mediaManager.dismissMediaData(key, 0L)
289             }
290         }
291     }
292 
293     private fun recreatePlayers() {
294         MediaPlayerData.mediaData().forEach { (key, data) ->
295             removePlayer(key, dismissMediaData = false)
296             addOrUpdatePlayer(key = key, oldKey = null, data = data)
297         }
298     }
299 
300     private fun updatePageIndicator() {
301         val numPages = mediaContent.getChildCount()
302         pageIndicator.setNumPages(numPages, Color.WHITE)
303         if (numPages == 1) {
304             pageIndicator.setLocation(0f)
305         }
306         updatePageIndicatorAlpha()
307     }
308 
309     /**
310      * Set a new interpolated state for all players. This is a state that is usually controlled
311      * by a finger movement where the user drags from one state to the next.
312      *
313      * @param startLocation the start location of our state or -1 if this is directly set
314      * @param endLocation the ending location of our state.
315      * @param progress the progress of the transition between startLocation and endlocation. If
316      *                 this is not a guided transformation, this will be 1.0f
317      * @param immediately should this state be applied immediately, canceling all animations?
318      */
319     fun setCurrentState(
320         @MediaLocation startLocation: Int,
321         @MediaLocation endLocation: Int,
322         progress: Float,
323         immediately: Boolean
324     ) {
325         if (startLocation != currentStartLocation ||
326                 endLocation != currentEndLocation ||
327                 progress != currentTransitionProgress ||
328                 immediately
329         ) {
330             currentStartLocation = startLocation
331             currentEndLocation = endLocation
332             currentTransitionProgress = progress
333             for (mediaPlayer in MediaPlayerData.players()) {
334                 updatePlayerToState(mediaPlayer, immediately)
335             }
336             maybeResetSettingsCog()
337             updatePageIndicatorAlpha()
338         }
339     }
340 
341     private fun updatePageIndicatorAlpha() {
342         val hostStates = mediaHostStatesManager.mediaHostStates
343         val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
344         val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
345         val startAlpha = if (startIsVisible) 1.0f else 0.0f
346         val endAlpha = if (endIsVisible) 1.0f else 0.0f
347         var alpha = 1.0f
348         if (!endIsVisible || !startIsVisible) {
349             var progress = currentTransitionProgress
350             if (!endIsVisible) {
351                 progress = 1.0f - progress
352             }
353             // Let's fade in quickly at the end where the view is visible
354             progress = MathUtils.constrain(
355                     MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress),
356                     0.0f,
357                     1.0f)
358             alpha = MathUtils.lerp(startAlpha, endAlpha, progress)
359         }
360         pageIndicator.alpha = alpha
361     }
362 
363     private fun updatePageIndicatorLocation() {
364         // Update the location of the page indicator, carousel clipping
365         val translationX = if (isRtl) {
366             (pageIndicator.width - currentCarouselWidth) / 2.0f
367         } else {
368             (currentCarouselWidth - pageIndicator.width) / 2.0f
369         }
370         pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
371         val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
372         pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height -
373                 layoutParams.bottomMargin).toFloat()
374     }
375 
376     /**
377      * Update the dimension of this carousel.
378      */
379     private fun updateCarouselDimensions() {
380         var width = 0
381         var height = 0
382         for (mediaPlayer in MediaPlayerData.players()) {
383             val controller = mediaPlayer.mediaViewController
384             // When transitioning the view to gone, the view gets smaller, but the translation
385             // Doesn't, let's add the translation
386             width = Math.max(width, controller.currentWidth + controller.translationX.toInt())
387             height = Math.max(height, controller.currentHeight + controller.translationY.toInt())
388         }
389         if (width != currentCarouselWidth || height != currentCarouselHeight) {
390             currentCarouselWidth = width
391             currentCarouselHeight = height
392             mediaCarouselScrollHandler.setCarouselBounds(
393                     currentCarouselWidth, currentCarouselHeight)
394             updatePageIndicatorLocation()
395         }
396     }
397 
398     private fun maybeResetSettingsCog() {
399         val hostStates = mediaHostStatesManager.mediaHostStates
400         val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia
401                 ?: true
402         val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia
403                 ?: endShowsActive
404         if (currentlyShowingOnlyActive != endShowsActive ||
405                 ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
406                             startShowsActive != endShowsActive)) {
407             // Whenever we're transitioning from between differing states or the endstate differs
408             // we reset the translation
409             currentlyShowingOnlyActive = endShowsActive
410             mediaCarouselScrollHandler.resetTranslation(animate = true)
411         }
412     }
413 
414     private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
415         mediaPlayer.mediaViewController.setCurrentState(
416                 startLocation = currentStartLocation,
417                 endLocation = currentEndLocation,
418                 transitionProgress = currentTransitionProgress,
419                 applyImmediately = noAnimation)
420     }
421 
422     /**
423      * The desired location of this view has changed. We should remeasure the view to match
424      * the new bounds and kick off bounds animations if necessary.
425      * If an animation is happening, an animation is kicked of externally, which sets a new
426      * current state until we reach the targetState.
427      *
428      * @param desiredLocation the location we're going to
429      * @param desiredHostState the target state we're transitioning to
430      * @param animate should this be animated
431      */
432     fun onDesiredLocationChanged(
433         desiredLocation: Int,
434         desiredHostState: MediaHostState?,
435         animate: Boolean,
436         duration: Long = 200,
437         startDelay: Long = 0
438     ) {
439         desiredHostState?.let {
440             // This is a hosting view, let's remeasure our players
441             this.desiredLocation = desiredLocation
442             this.desiredHostState = it
443             currentlyExpanded = it.expansion > 0
444             for (mediaPlayer in MediaPlayerData.players()) {
445                 if (animate) {
446                     mediaPlayer.mediaViewController.animatePendingStateChange(
447                             duration = duration,
448                             delay = startDelay)
449                 }
450                 mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
451             }
452             mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
453             mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
454             val nowVisible = it.visible
455             if (nowVisible != playersVisible) {
456                 playersVisible = nowVisible
457                 if (nowVisible) {
458                     mediaCarouselScrollHandler.resetTranslation()
459                 }
460             }
461             updateCarouselSize()
462         }
463     }
464 
465     fun closeGuts() {
466         MediaPlayerData.players().forEach {
467             it.closeGuts(true)
468         }
469     }
470 
471     /**
472      * Update the size of the carousel, remeasuring it if necessary.
473      */
474     private fun updateCarouselSize() {
475         val width = desiredHostState?.measurementInput?.width ?: 0
476         val height = desiredHostState?.measurementInput?.height ?: 0
477         if (width != carouselMeasureWidth && width != 0 ||
478                 height != carouselMeasureHeight && height != 0) {
479             carouselMeasureWidth = width
480             carouselMeasureHeight = height
481             val playerWidthPlusPadding = carouselMeasureWidth +
482                     context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
483             // Let's remeasure the carousel
484             val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
485             val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
486             mediaCarousel.measure(widthSpec, heightSpec)
487             mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
488             // Update the padding after layout; view widths are used in RTL to calculate scrollX
489             mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
490         }
491     }
492 }
493 
494 @VisibleForTesting
495 internal object MediaPlayerData {
496     private data class MediaSortKey(
497         val data: MediaData,
498         val updateTime: Long = 0
499     )
500 
501     private val comparator =
<lambda>null502         compareByDescending<MediaSortKey> { it.data.isPlaying }
<lambda>null503         .thenByDescending { it.data.isLocalSession }
<lambda>null504         .thenByDescending { !it.data.resumption }
<lambda>null505         .thenByDescending { it.updateTime }
506 
507     private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
508     private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
509 
addMediaPlayernull510     fun addMediaPlayer(key: String, data: MediaData, player: MediaControlPanel) {
511         removeMediaPlayer(key)
512         val sortKey = MediaSortKey(data, System.currentTimeMillis())
513         mediaData.put(key, sortKey)
514         mediaPlayers.put(sortKey, player)
515     }
516 
getMediaPlayernull517     fun getMediaPlayer(key: String, oldKey: String?): MediaControlPanel? {
518         // If the key was changed, update entry
519         oldKey?.let {
520             if (it != key) {
521                 mediaData.remove(it)?.let { sortKey -> mediaData.put(key, sortKey) }
522             }
523         }
524         return mediaData.get(key)?.let { mediaPlayers.get(it) }
525     }
526 
<lambda>null527     fun removeMediaPlayer(key: String) = mediaData.remove(key)?.let { mediaPlayers.remove(it) }
528 
mediaDatanull529     fun mediaData() = mediaData.entries.map { e -> Pair(e.key, e.value.data) }
530 
playersnull531     fun players() = mediaPlayers.values
532 
533     @VisibleForTesting
534     fun clear() {
535         mediaData.clear()
536         mediaPlayers.clear()
537     }
538 }
539