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