<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