blob: 49d2d8860a2fa6777047314582c6dc86923a00e3 [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()
45 private val visualStabilityCallback = ::reorderAllPlayers
Selim Cinekf418bb02020-05-04 17:16:58 -070046 private var activeMediaIndex: Int = 0
47 private var scrollIntoCurrentMedia: Int = 0
Selim Cinek9e517f72020-04-28 12:24:34 -070048
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 {
59 override fun onScrollChange(v: View?, scrollX: Int, scrollY: Int, oldScrollX: Int,
60 oldScrollY: Int) {
61 if (playerWidthPlusPadding == 0) {
62 return
63 }
64 onMediaScrollingChanged(scrollX / playerWidthPlusPadding,
65 scrollX % playerWidthPlusPadding)
66 }
67 }
Selim Cinek9e517f72020-04-28 12:24:34 -070068
69 init {
70 mediaCarousel = inflateMediaCarousel()
Selim Cinekf418bb02020-05-04 17:16:58 -070071 mediaCarousel.setOnScrollChangeListener(scrollChangedListener)
Selim Cinek9e517f72020-04-28 12:24:34 -070072 mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
73 mediaManager.addListener(object : MediaDataManager.Listener {
74 override fun onMediaDataLoaded(key: String, data: MediaData) {
75 updateView(key, data)
Selim Cinekf418bb02020-05-04 17:16:58 -070076 updatePlayerVisibilities()
Selim Cinek9e517f72020-04-28 12:24:34 -070077 }
78
79 override fun onMediaDataRemoved(key: String) {
80 val removed = mediaPlayers.remove(key)
81 removed?.apply {
Selim Cinekf418bb02020-05-04 17:16:58 -070082 val beforeActive = mediaContent.indexOfChild(removed.view) <= activeMediaIndex
Selim Cinek9e517f72020-04-28 12:24:34 -070083 mediaContent.removeView(removed.view)
84 removed.onDestroy()
Selim Cinekb28ec0a2020-05-01 15:07:42 -070085 updateMediaPaddings()
Selim Cinekf418bb02020-05-04 17:16:58 -070086 if (beforeActive) {
87 // also update the index here since the scroll below might not always lead
88 // to a scrolling changed
89 activeMediaIndex = Math.max(0, activeMediaIndex - 1)
90 mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX
91 - playerWidthPlusPadding, 0)
92 }
93 updatePlayerVisibilities()
Selim Cinek9e517f72020-04-28 12:24:34 -070094 }
95 }
96 })
97 }
98
Selim Cinekf418bb02020-05-04 17:16:58 -070099 private fun inflateMediaCarousel(): HorizontalScrollView {
100 return LayoutInflater.from(context).inflate(R.layout.media_carousel,
101 UniqueObjectHostView(context), false) as HorizontalScrollView
Selim Cinek9e517f72020-04-28 12:24:34 -0700102 }
103
104 private fun reorderAllPlayers() {
105 for (mediaPlayer in mediaPlayers.values) {
106 val view = mediaPlayer.view
107 if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) {
108 mediaContent.removeView(view)
109 mediaContent.addView(view, 0)
110 }
111 }
112 updateMediaPaddings()
Selim Cinekf418bb02020-05-04 17:16:58 -0700113 updatePlayerVisibilities()
114 }
115
116 private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
117 val wasScrolledIn = scrollIntoCurrentMedia != 0
118 scrollIntoCurrentMedia = scrollInAmount
119 val nowScrolledIn = scrollIntoCurrentMedia != 0
120 if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
121 activeMediaIndex = newIndex
122 updatePlayerVisibilities()
123 }
124 }
125
126 private fun updatePlayerVisibilities() {
127 val scrolledIn = scrollIntoCurrentMedia != 0
128 for (i in 0 until mediaContent.childCount) {
129 val view = mediaContent.getChildAt(i)
130 val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn)
131 view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
132 }
Selim Cinek9e517f72020-04-28 12:24:34 -0700133 }
134
135 private fun updateView(key: String, data: MediaData) {
136 var existingPlayer = mediaPlayers[key]
137 if (existingPlayer == null) {
138 // Set up listener for device changes
139 // TODO: integrate with MediaTransferManager?
140 val imm = InfoMediaManager(context, data.packageName,
141 null /* notification */, localBluetoothManager)
142 val routeManager = LocalMediaManager(context, localBluetoothManager,
143 imm, data.packageName)
144
145 existingPlayer = MediaControlPanel(context, mediaContent, routeManager,
146 foregroundExecutor, backgroundExecutor, activityStarter)
147 mediaPlayers[key] = existingPlayer
148 val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
149 ViewGroup.LayoutParams.WRAP_CONTENT)
150 existingPlayer.view.setLayoutParams(lp)
Selim Cinek54809622020-04-30 19:04:44 -0700151 existingPlayer.setListening(currentlyExpanded)
Selim Cinek9e517f72020-04-28 12:24:34 -0700152 if (existingPlayer.isPlaying) {
153 mediaContent.addView(existingPlayer.view, 0)
154 } else {
155 mediaContent.addView(existingPlayer.view)
156 }
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700157 updatePlayerToCurrentState(existingPlayer)
Selim Cinek9e517f72020-04-28 12:24:34 -0700158 } else if (existingPlayer.isPlaying &&
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700159 mediaContent.indexOfChild(existingPlayer.view) != 0) {
Selim Cinek9e517f72020-04-28 12:24:34 -0700160 if (visualStabilityManager.isReorderingAllowed) {
161 mediaContent.removeView(existingPlayer.view)
162 mediaContent.addView(existingPlayer.view, 0)
163 } else {
164 visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback)
165 }
166 }
167 existingPlayer.bind(data)
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700168 // Resetting the progress to make sure it's taken into account for the latest
169 // motion model
170 existingPlayer.view.progress = currentState?.expansion ?: 0.0f
Selim Cinek9e517f72020-04-28 12:24:34 -0700171 updateMediaPaddings()
172 }
173
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700174 private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) {
175 if (desiredState != null && desiredState!!.measurementInput != null) {
176 // make sure the player width is set to the current state
Selim Cinekf418bb02020-05-04 17:16:58 -0700177 existingPlayer.setPlayerWidth(playerWidth)
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700178 }
179 }
180
Selim Cinek9e517f72020-04-28 12:24:34 -0700181 private fun updateMediaPaddings() {
182 val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
183 val childCount = mediaContent.childCount
184 for (i in 0 until childCount) {
185 val mediaView = mediaContent.getChildAt(i)
186 val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
187 val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
188 if (layoutParams.marginEnd != desiredPaddingEnd) {
189 layoutParams.marginEnd = desiredPaddingEnd
190 mediaView.layoutParams = layoutParams
191 }
192 }
193
194 }
195
Selim Cinekf418bb02020-05-04 17:16:58 -0700196 /**
197 * Set the current state of a view. This is updated often during animations and we shouldn't
198 * do anything expensive.
199 */
Selim Cinek54809622020-04-30 19:04:44 -0700200 fun setCurrentState(state: MediaState) {
201 currentState = state
202 currentlyExpanded = state.expansion > 0
Selim Cinek9e517f72020-04-28 12:24:34 -0700203 for (mediaPlayer in mediaPlayers.values) {
204 val view = mediaPlayer.view
205 view.progress = state.expansion
206 }
Selim Cinek9e517f72020-04-28 12:24:34 -0700207 }
Selim Cinek3df592e2020-04-28 13:51:43 -0700208
209 /**
Selim Cinekf418bb02020-05-04 17:16:58 -0700210 * The desired location of this view has changed. We should remeasure the view to match
211 * the new bounds and kick off bounds animations if necessary.
212 * If an animation is happening, an animation is kicked of externally, which sets a new
213 * current state until we reach the targetState.
214 *
215 * @param desiredState the target state we're transitioning to
Selim Cinek3df592e2020-04-28 13:51:43 -0700216 * @param animate should this be animated
217 */
Selim Cinekf418bb02020-05-04 17:16:58 -0700218 fun onDesiredLocationChanged(desiredState: MediaState?, animate: Boolean, duration: Long,
219 startDelay: Long) {
220 if (desiredState is MediaHost.MediaHostState) {
Selim Cinek54809622020-04-30 19:04:44 -0700221 // This is a hosting view, let's remeasure our players
Selim Cinekf418bb02020-05-04 17:16:58 -0700222 this.desiredState = desiredState
223 val width = desiredState.boundsOnScreen.width()
224 if (playerWidth != width) {
225 setPlayerWidth(width)
226 for (mediaPlayer in mediaPlayers.values) {
227 if (animate && mediaPlayer.view.visibility == View.VISIBLE) {
228 mediaPlayer.animatePendingSizeChange(duration, startDelay)
229 }
230 }
231 val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0
232 val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0
233 var left = 0
234 for (i in 0 until mediaContent.childCount) {
235 val view = mediaContent.getChildAt(i)
236 view.measure(widthSpec, heightSpec)
237 view.layout(left, 0, left + width, view.measuredHeight)
238 left = left + playerWidthPlusPadding
239 }
240 }
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700241 }
242 }
243
Selim Cinekf418bb02020-05-04 17:16:58 -0700244 fun setPlayerWidth(width: Int) {
245 if (width != playerWidth) {
246 playerWidth = width
247 playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize(
248 R.dimen.qs_media_padding)
249 for (mediaPlayer in mediaPlayers.values) {
250 mediaPlayer.setPlayerWidth(width)
251 }
252 // The player width has changed, let's update the scroll position to make sure
253 // it's still at the same place
254 var newScroll = activeMediaIndex * playerWidthPlusPadding
255 if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
256 newScroll += playerWidthPlusPadding
257 - (scrollIntoCurrentMedia - playerWidthPlusPadding)
258 } else {
259 newScroll += scrollIntoCurrentMedia
260 }
261 mediaCarousel.scrollX = newScroll
Selim Cinek3df592e2020-04-28 13:51:43 -0700262 }
Selim Cinek3df592e2020-04-28 13:51:43 -0700263 }
264
Selim Cinek54809622020-04-30 19:04:44 -0700265 /**
266 * Get a measurement for the given input state. This measures the first player and returns
267 * its bounds as if it were measured with the given measurement dimensions
268 */
269 fun obtainMeasurement(input: MediaMeasurementInput) : MeasurementOutput? {
270 val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null
271 // Let's measure the size of the first player and return its height
272 val previousProgress = firstPlayer.view.progress
Selim Cinekf418bb02020-05-04 17:16:58 -0700273 val previousRight = firstPlayer.view.right
274 val previousBottom = firstPlayer.view.bottom
Selim Cinek54809622020-04-30 19:04:44 -0700275 firstPlayer.view.progress = input.expansion
Selim Cinekf418bb02020-05-04 17:16:58 -0700276 firstPlayer.measure(input)
277 // Relayouting is necessary in motionlayout to obtain its size properly ....
278 firstPlayer.view.layout(0, 0, firstPlayer.view.measuredWidth,
279 firstPlayer.view.measuredHeight)
Selim Cinek54809622020-04-30 19:04:44 -0700280 val result = MeasurementOutput(firstPlayer.view.measuredWidth,
281 firstPlayer.view.measuredHeight)
282 firstPlayer.view.progress = previousProgress
283 if (desiredState != null) {
284 // remeasure it to the old size again!
Selim Cinekf418bb02020-05-04 17:16:58 -0700285 firstPlayer.measure(desiredState!!.measurementInput)
286 firstPlayer.view.layout(0, 0, previousRight, previousBottom)
Selim Cinek3df592e2020-04-28 13:51:43 -0700287 }
Selim Cinek54809622020-04-30 19:04:44 -0700288 return result
Selim Cinek3df592e2020-04-28 13:51:43 -0700289 }
Selim Cinekf418bb02020-05-04 17:16:58 -0700290
291 fun onViewReattached() {
292 if (desiredState is MediaHost.MediaHostState) {
293 // HACK: MotionLayout doesn't always properly reevalate the state, let's kick of
294 // a measure to force it.
295 val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0
296 val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0
297 for (mediaPlayer in mediaPlayers.values) {
298 mediaPlayer.view.measure(widthSpec, heightSpec)
299 }
300 }
301 }
Selim Cinek9e517f72020-04-28 12:24:34 -0700302}