blob: d72c3691c34b6e89f05aa1be97b27a64db2a644b [file] [log] [blame]
Selim Cinek9e517f72020-04-28 12:24:34 -07001package com.android.systemui.media
2
3import android.content.Context
4import android.view.LayoutInflater
Selim Cinekf418bb02020-05-04 17:16:58 -07005import android.view.View
Selim Cinek9e517f72020-04-28 12:24:34 -07006import android.view.ViewGroup
Selim Cinekf418bb02020-05-04 17:16:58 -07007import android.widget.HorizontalScrollView
Selim Cinek9e517f72020-04-28 12:24:34 -07008import android.widget.LinearLayout
Selim Cinek9e517f72020-04-28 12:24:34 -07009import com.android.settingslib.bluetooth.LocalBluetoothManager
10import com.android.settingslib.media.InfoMediaManager
11import com.android.settingslib.media.LocalMediaManager
12import com.android.systemui.R
13import com.android.systemui.dagger.qualifiers.Background
14import com.android.systemui.dagger.qualifiers.Main
15import com.android.systemui.plugins.ActivityStarter
16import com.android.systemui.statusbar.notification.VisualStabilityManager
Selim Cinek54809622020-04-30 19:04:44 -070017import com.android.systemui.util.animation.MeasurementOutput
18import com.android.systemui.util.animation.UniqueObjectHostView
Selim Cinek9e517f72020-04-28 12:24:34 -070019import com.android.systemui.util.concurrency.DelayableExecutor
20import java.util.concurrent.Executor
21import javax.inject.Inject
22import javax.inject.Singleton
23
24/**
25 * Class that is responsible for keeping the view carousel up to date.
26 * This also handles changes in state and applies them to the media carousel like the expansion.
27 */
28@Singleton
29class MediaViewManager @Inject constructor(
30 private val context: Context,
31 @Main private val foregroundExecutor: Executor,
32 @Background private val backgroundExecutor: DelayableExecutor,
33 private val localBluetoothManager: LocalBluetoothManager?,
34 private val visualStabilityManager: VisualStabilityManager,
35 private val activityStarter: ActivityStarter,
36 mediaManager: MediaDataManager
37) {
Selim Cinekf418bb02020-05-04 17:16:58 -070038 private var playerWidth: Int = 0
39 private var playerWidthPlusPadding: Int = 0
Selim Cinek54809622020-04-30 19:04:44 -070040 private var desiredState: MediaHost.MediaHostState? = null
41 private var currentState: MediaState? = null
Selim Cinekf418bb02020-05-04 17:16:58 -070042 val mediaCarousel: HorizontalScrollView
Selim Cinek9e517f72020-04-28 12:24:34 -070043 private val mediaContent: ViewGroup
44 private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
Selim Cinek0260c752020-05-11 16:03:52 -070045 private val visualStabilityCallback : VisualStabilityManager.Callback
Selim Cinekf418bb02020-05-04 17:16:58 -070046 private var activeMediaIndex: Int = 0
Selim Cinek0260c752020-05-11 16:03:52 -070047 private var needsReordering: Boolean = false
Selim Cinekf418bb02020-05-04 17:16:58 -070048 private var scrollIntoCurrentMedia: Int = 0
Selim Cinek54809622020-04-30 19:04:44 -070049 private var currentlyExpanded = true
Selim Cinek9e517f72020-04-28 12:24:34 -070050 set(value) {
51 if (field != value) {
52 field = value
53 for (player in mediaPlayers.values) {
54 player.setListening(field)
55 }
56 }
57 }
Selim Cinekf418bb02020-05-04 17:16:58 -070058 private val scrollChangedListener = object : View.OnScrollChangeListener {
Robert Snoebergereb49e942020-05-12 16:31:09 -040059 override fun onScrollChange(
60 v: View?,
61 scrollX: Int,
62 scrollY: Int,
63 oldScrollX: Int,
64 oldScrollY: Int
65 ) {
Selim Cinekf418bb02020-05-04 17:16:58 -070066 if (playerWidthPlusPadding == 0) {
67 return
68 }
69 onMediaScrollingChanged(scrollX / playerWidthPlusPadding,
70 scrollX % playerWidthPlusPadding)
71 }
72 }
Selim Cinek9e517f72020-04-28 12:24:34 -070073
74 init {
75 mediaCarousel = inflateMediaCarousel()
Selim Cinekf418bb02020-05-04 17:16:58 -070076 mediaCarousel.setOnScrollChangeListener(scrollChangedListener)
Selim Cinek9e517f72020-04-28 12:24:34 -070077 mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
Selim Cinek0260c752020-05-11 16:03:52 -070078 visualStabilityCallback = VisualStabilityManager.Callback {
79 if (needsReordering) {
80 needsReordering = false
81 reorderAllPlayers()
82 }
83 // Let's reset our scroll position
84 mediaCarousel.scrollX = 0
85 }
86 visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback,
87 true /* persistent */)
Selim Cinek9e517f72020-04-28 12:24:34 -070088 mediaManager.addListener(object : MediaDataManager.Listener {
89 override fun onMediaDataLoaded(key: String, data: MediaData) {
90 updateView(key, data)
Selim Cinekf418bb02020-05-04 17:16:58 -070091 updatePlayerVisibilities()
Selim Cinek9e517f72020-04-28 12:24:34 -070092 }
93
94 override fun onMediaDataRemoved(key: String) {
95 val removed = mediaPlayers.remove(key)
96 removed?.apply {
Robert Snoebergereb49e942020-05-12 16:31:09 -040097 val beforeActive = mediaContent.indexOfChild(removed.view?.player) <=
98 activeMediaIndex
99 mediaContent.removeView(removed.view?.player)
Selim Cinek9e517f72020-04-28 12:24:34 -0700100 removed.onDestroy()
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700101 updateMediaPaddings()
Selim Cinekf418bb02020-05-04 17:16:58 -0700102 if (beforeActive) {
103 // also update the index here since the scroll below might not always lead
104 // to a scrolling changed
105 activeMediaIndex = Math.max(0, activeMediaIndex - 1)
Robert Snoebergereb49e942020-05-12 16:31:09 -0400106 mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX -
107 playerWidthPlusPadding, 0)
Selim Cinekf418bb02020-05-04 17:16:58 -0700108 }
109 updatePlayerVisibilities()
Selim Cinek9e517f72020-04-28 12:24:34 -0700110 }
111 }
112 })
113 }
114
Selim Cinekf418bb02020-05-04 17:16:58 -0700115 private fun inflateMediaCarousel(): HorizontalScrollView {
116 return LayoutInflater.from(context).inflate(R.layout.media_carousel,
117 UniqueObjectHostView(context), false) as HorizontalScrollView
Selim Cinek9e517f72020-04-28 12:24:34 -0700118 }
119
120 private fun reorderAllPlayers() {
121 for (mediaPlayer in mediaPlayers.values) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400122 val view = mediaPlayer.view?.player
Selim Cinek9e517f72020-04-28 12:24:34 -0700123 if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) {
124 mediaContent.removeView(view)
125 mediaContent.addView(view, 0)
126 }
127 }
128 updateMediaPaddings()
Selim Cinekf418bb02020-05-04 17:16:58 -0700129 updatePlayerVisibilities()
130 }
131
132 private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
133 val wasScrolledIn = scrollIntoCurrentMedia != 0
134 scrollIntoCurrentMedia = scrollInAmount
135 val nowScrolledIn = scrollIntoCurrentMedia != 0
136 if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
137 activeMediaIndex = newIndex
138 updatePlayerVisibilities()
139 }
140 }
141
142 private fun updatePlayerVisibilities() {
143 val scrolledIn = scrollIntoCurrentMedia != 0
144 for (i in 0 until mediaContent.childCount) {
145 val view = mediaContent.getChildAt(i)
146 val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn)
147 view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
148 }
Selim Cinek9e517f72020-04-28 12:24:34 -0700149 }
150
151 private fun updateView(key: String, data: MediaData) {
152 var existingPlayer = mediaPlayers[key]
153 if (existingPlayer == null) {
154 // Set up listener for device changes
155 // TODO: integrate with MediaTransferManager?
156 val imm = InfoMediaManager(context, data.packageName,
157 null /* notification */, localBluetoothManager)
158 val routeManager = LocalMediaManager(context, localBluetoothManager,
159 imm, data.packageName)
160
Robert Snoebergereb49e942020-05-12 16:31:09 -0400161 existingPlayer = MediaControlPanel(context, routeManager, foregroundExecutor,
162 backgroundExecutor, activityStarter)
163 existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
164 mediaContent))
Selim Cinek9e517f72020-04-28 12:24:34 -0700165 mediaPlayers[key] = existingPlayer
166 val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
167 ViewGroup.LayoutParams.WRAP_CONTENT)
Robert Snoebergereb49e942020-05-12 16:31:09 -0400168 existingPlayer.view?.player?.setLayoutParams(lp)
Selim Cinek54809622020-04-30 19:04:44 -0700169 existingPlayer.setListening(currentlyExpanded)
Selim Cinek9e517f72020-04-28 12:24:34 -0700170 if (existingPlayer.isPlaying) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400171 mediaContent.addView(existingPlayer.view?.player, 0)
Selim Cinek9e517f72020-04-28 12:24:34 -0700172 } else {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400173 mediaContent.addView(existingPlayer.view?.player)
Selim Cinek9e517f72020-04-28 12:24:34 -0700174 }
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700175 updatePlayerToCurrentState(existingPlayer)
Selim Cinek9e517f72020-04-28 12:24:34 -0700176 } else if (existingPlayer.isPlaying &&
Robert Snoebergereb49e942020-05-12 16:31:09 -0400177 mediaContent.indexOfChild(existingPlayer.view?.player) != 0) {
Selim Cinek9e517f72020-04-28 12:24:34 -0700178 if (visualStabilityManager.isReorderingAllowed) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400179 mediaContent.removeView(existingPlayer.view?.player)
180 mediaContent.addView(existingPlayer.view?.player, 0)
Selim Cinek9e517f72020-04-28 12:24:34 -0700181 } else {
Selim Cinek0260c752020-05-11 16:03:52 -0700182 needsReordering = true
Selim Cinek9e517f72020-04-28 12:24:34 -0700183 }
184 }
185 existingPlayer.bind(data)
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700186 // Resetting the progress to make sure it's taken into account for the latest
187 // motion model
Robert Snoebergereb49e942020-05-12 16:31:09 -0400188 existingPlayer.view?.player?.progress = currentState?.expansion ?: 0.0f
Selim Cinek9e517f72020-04-28 12:24:34 -0700189 updateMediaPaddings()
190 }
191
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700192 private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) {
193 if (desiredState != null && desiredState!!.measurementInput != null) {
194 // make sure the player width is set to the current state
Selim Cinekf418bb02020-05-04 17:16:58 -0700195 existingPlayer.setPlayerWidth(playerWidth)
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700196 }
197 }
198
Selim Cinek9e517f72020-04-28 12:24:34 -0700199 private fun updateMediaPaddings() {
200 val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
201 val childCount = mediaContent.childCount
202 for (i in 0 until childCount) {
203 val mediaView = mediaContent.getChildAt(i)
204 val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
205 val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
206 if (layoutParams.marginEnd != desiredPaddingEnd) {
207 layoutParams.marginEnd = desiredPaddingEnd
208 mediaView.layoutParams = layoutParams
209 }
210 }
Selim Cinek9e517f72020-04-28 12:24:34 -0700211 }
212
Selim Cinekf418bb02020-05-04 17:16:58 -0700213 /**
214 * Set the current state of a view. This is updated often during animations and we shouldn't
215 * do anything expensive.
216 */
Selim Cinek54809622020-04-30 19:04:44 -0700217 fun setCurrentState(state: MediaState) {
218 currentState = state
219 currentlyExpanded = state.expansion > 0
Selim Cinek9e517f72020-04-28 12:24:34 -0700220 for (mediaPlayer in mediaPlayers.values) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400221 val view = mediaPlayer.view?.player
222 view?.progress = state.expansion
Selim Cinek9e517f72020-04-28 12:24:34 -0700223 }
Selim Cinek9e517f72020-04-28 12:24:34 -0700224 }
Selim Cinek3df592e2020-04-28 13:51:43 -0700225
226 /**
Selim Cinekf418bb02020-05-04 17:16:58 -0700227 * The desired location of this view has changed. We should remeasure the view to match
228 * the new bounds and kick off bounds animations if necessary.
229 * If an animation is happening, an animation is kicked of externally, which sets a new
230 * current state until we reach the targetState.
231 *
232 * @param desiredState the target state we're transitioning to
Selim Cinek3df592e2020-04-28 13:51:43 -0700233 * @param animate should this be animated
234 */
Robert Snoebergereb49e942020-05-12 16:31:09 -0400235 fun onDesiredLocationChanged(
236 desiredState: MediaState?,
237 animate: Boolean,
238 duration: Long,
239 startDelay: Long
240 ) {
Selim Cinekf418bb02020-05-04 17:16:58 -0700241 if (desiredState is MediaHost.MediaHostState) {
Selim Cinek54809622020-04-30 19:04:44 -0700242 // This is a hosting view, let's remeasure our players
Selim Cinekf418bb02020-05-04 17:16:58 -0700243 this.desiredState = desiredState
244 val width = desiredState.boundsOnScreen.width()
245 if (playerWidth != width) {
246 setPlayerWidth(width)
247 for (mediaPlayer in mediaPlayers.values) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400248 if (animate && mediaPlayer.view?.player?.visibility == View.VISIBLE) {
Selim Cinekf418bb02020-05-04 17:16:58 -0700249 mediaPlayer.animatePendingSizeChange(duration, startDelay)
250 }
251 }
252 val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0
253 val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0
254 var left = 0
255 for (i in 0 until mediaContent.childCount) {
256 val view = mediaContent.getChildAt(i)
257 view.measure(widthSpec, heightSpec)
258 view.layout(left, 0, left + width, view.measuredHeight)
259 left = left + playerWidthPlusPadding
260 }
261 }
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700262 }
263 }
264
Selim Cinekf418bb02020-05-04 17:16:58 -0700265 fun setPlayerWidth(width: Int) {
266 if (width != playerWidth) {
267 playerWidth = width
268 playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize(
269 R.dimen.qs_media_padding)
270 for (mediaPlayer in mediaPlayers.values) {
271 mediaPlayer.setPlayerWidth(width)
272 }
273 // The player width has changed, let's update the scroll position to make sure
274 // it's still at the same place
275 var newScroll = activeMediaIndex * playerWidthPlusPadding
276 if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
277 newScroll += playerWidthPlusPadding
278 - (scrollIntoCurrentMedia - playerWidthPlusPadding)
279 } else {
280 newScroll += scrollIntoCurrentMedia
281 }
282 mediaCarousel.scrollX = newScroll
Selim Cinek3df592e2020-04-28 13:51:43 -0700283 }
Selim Cinek3df592e2020-04-28 13:51:43 -0700284 }
285
Selim Cinek54809622020-04-30 19:04:44 -0700286 /**
287 * Get a measurement for the given input state. This measures the first player and returns
288 * its bounds as if it were measured with the given measurement dimensions
289 */
Robert Snoebergereb49e942020-05-12 16:31:09 -0400290 fun obtainMeasurement(input: MediaMeasurementInput): MeasurementOutput? {
Selim Cinek54809622020-04-30 19:04:44 -0700291 val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null
Robert Snoebergereb49e942020-05-12 16:31:09 -0400292 var result: MeasurementOutput? = null
293 firstPlayer.view?.player?.let {
294 // Let's measure the size of the first player and return its height
295 val previousProgress = it.progress
296 val previousRight = it.right
297 val previousBottom = it.bottom
298 it.progress = input.expansion
299 firstPlayer.measure(input)
300 // Relayouting is necessary in motionlayout to obtain its size properly ....
301 it.layout(0, 0, it.measuredWidth, it.measuredHeight)
302 val result = MeasurementOutput(it.measuredWidth, it.measuredHeight)
303 it.progress = previousProgress
304 if (desiredState != null) {
305 // remeasure it to the old size again!
306 firstPlayer.measure(desiredState!!.measurementInput)
307 it.layout(0, 0, previousRight, previousBottom)
308 }
Selim Cinek3df592e2020-04-28 13:51:43 -0700309 }
Selim Cinek54809622020-04-30 19:04:44 -0700310 return result
Selim Cinek3df592e2020-04-28 13:51:43 -0700311 }
Selim Cinekf418bb02020-05-04 17:16:58 -0700312
313 fun onViewReattached() {
314 if (desiredState is MediaHost.MediaHostState) {
315 // HACK: MotionLayout doesn't always properly reevalate the state, let's kick of
316 // a measure to force it.
317 val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0
318 val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0
319 for (mediaPlayer in mediaPlayers.values) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400320 mediaPlayer.view?.player?.measure(widthSpec, heightSpec)
Selim Cinekf418bb02020-05-04 17:16:58 -0700321 }
322 }
323 }
Robert Snoebergereb49e942020-05-12 16:31:09 -0400324}