1 package com.android.systemui.media
2
3 import android.graphics.Rect
4 import android.util.ArraySet
5 import android.view.View
6 import android.view.View.OnAttachStateChangeListener
7 import com.android.systemui.util.animation.DisappearParameters
8 import com.android.systemui.util.animation.MeasurementInput
9 import com.android.systemui.util.animation.MeasurementOutput
10 import com.android.systemui.util.animation.UniqueObjectHostView
11 import java.util.Objects
12 import javax.inject.Inject
13
14 class MediaHost constructor(
15 private val state: MediaHostStateHolder,
16 private val mediaHierarchyManager: MediaHierarchyManager,
17 private val mediaDataManager: MediaDataManager,
18 private val mediaHostStatesManager: MediaHostStatesManager
<lambda>null19 ) : MediaHostState by state {
20 lateinit var hostView: UniqueObjectHostView
21 var location: Int = -1
22 private set
23 private var visibleChangedListeners: ArraySet<(Boolean) -> Unit> = ArraySet()
24
25 private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
26
27 private var inited: Boolean = false
28
29 /**
30 * Are we listening to media data changes?
31 */
32 private var listeningToMediaData = false
33
34 /**
35 * Get the current bounds on the screen. This makes sure the state is fresh and up to date
36 */
37 val currentBounds: Rect = Rect()
38 get() {
39 hostView.getLocationOnScreen(tmpLocationOnScreen)
40 var left = tmpLocationOnScreen[0] + hostView.paddingLeft
41 var top = tmpLocationOnScreen[1] + hostView.paddingTop
42 var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight
43 var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom
44 // Handle cases when the width or height is 0 but it has padding. In those cases
45 // the above could return negative widths, which is wrong
46 if (right < left) {
47 left = 0
48 right = 0
49 }
50 if (bottom < top) {
51 bottom = 0
52 top = 0
53 }
54 field.set(left, top, right, bottom)
55 return field
56 }
57
58 private val listener = object : MediaDataManager.Listener {
59 override fun onMediaDataLoaded(
60 key: String,
61 oldKey: String?,
62 data: MediaData,
63 immediately: Boolean,
64 isSsReactivated: Boolean
65 ) {
66 if (immediately) {
67 updateViewVisibility()
68 }
69 }
70
71 override fun onSmartspaceMediaDataLoaded(
72 key: String,
73 data: SmartspaceMediaData,
74 shouldPrioritize: Boolean
75 ) {
76 updateViewVisibility()
77 }
78
79 override fun onMediaDataRemoved(key: String) {
80 updateViewVisibility()
81 }
82
83 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
84 if (immediately) {
85 updateViewVisibility()
86 }
87 }
88 }
89
90 fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
91 visibleChangedListeners.add(listener)
92 }
93
94 fun removeVisibilityChangeListener(listener: (Boolean) -> Unit) {
95 visibleChangedListeners.remove(listener)
96 }
97
98 /**
99 * Initialize this MediaObject and create a host view.
100 * All state should already be set on this host before calling this method in order to avoid
101 * unnecessary state changes which lead to remeasurings later on.
102 *
103 * @param location the location this host name has. Used to identify the host during
104 * transitions.
105 */
106 fun init(@MediaLocation location: Int) {
107 if (inited) {
108 return
109 }
110 inited = true
111
112 this.location = location
113 hostView = mediaHierarchyManager.register(this)
114 // Listen by default, as the host might not be attached by our clients, until
115 // they get a visibility change. We still want to stay up to date in that case!
116 setListeningToMediaData(true)
117 hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
118 override fun onViewAttachedToWindow(v: View?) {
119 setListeningToMediaData(true)
120 updateViewVisibility()
121 }
122
123 override fun onViewDetachedFromWindow(v: View?) {
124 setListeningToMediaData(false)
125 }
126 })
127
128 // Listen to measurement updates and update our state with it
129 hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager {
130 override fun onMeasure(input: MeasurementInput): MeasurementOutput {
131 // Modify the measurement to exactly match the dimensions
132 if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) {
133 input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
134 View.MeasureSpec.getSize(input.widthMeasureSpec),
135 View.MeasureSpec.EXACTLY)
136 }
137 // This will trigger a state change that ensures that we now have a state available
138 state.measurementInput = input
139 return mediaHostStatesManager.updateCarouselDimensions(location, state)
140 }
141 }
142
143 // Whenever the state changes, let our state manager know
144 state.changedListener = {
145 mediaHostStatesManager.updateHostState(location, state)
146 }
147
148 updateViewVisibility()
149 }
150
151 private fun setListeningToMediaData(listen: Boolean) {
152 if (listen != listeningToMediaData) {
153 listeningToMediaData = listen
154 if (listen) {
155 mediaDataManager.addListener(listener)
156 } else {
157 mediaDataManager.removeListener(listener)
158 }
159 }
160 }
161
162 private fun updateViewVisibility() {
163 state.visible = if (showsOnlyActiveMedia) {
164 mediaDataManager.hasActiveMedia()
165 } else {
166 mediaDataManager.hasAnyMedia()
167 }
168 val newVisibility = if (visible) View.VISIBLE else View.GONE
169 if (newVisibility != hostView.visibility) {
170 hostView.visibility = newVisibility
171 visibleChangedListeners.forEach {
172 it.invoke(visible)
173 }
174 }
175 }
176
177 class MediaHostStateHolder @Inject constructor() : MediaHostState {
178 override var measurementInput: MeasurementInput? = null
179 set(value) {
180 if (value?.equals(field) != true) {
181 field = value
182 changedListener?.invoke()
183 }
184 }
185
186 override var expansion: Float = 0.0f
187 set(value) {
188 if (!value.equals(field)) {
189 field = value
190 changedListener?.invoke()
191 }
192 }
193
194 override var showsOnlyActiveMedia: Boolean = false
195 set(value) {
196 if (!value.equals(field)) {
197 field = value
198 changedListener?.invoke()
199 }
200 }
201
202 override var visible: Boolean = true
203 set(value) {
204 if (field == value) {
205 return
206 }
207 field = value
208 changedListener?.invoke()
209 }
210
211 override var falsingProtectionNeeded: Boolean = false
212 set(value) {
213 if (field == value) {
214 return
215 }
216 field = value
217 changedListener?.invoke()
218 }
219
220 override var disappearParameters: DisappearParameters = DisappearParameters()
221 set(value) {
222 val newHash = value.hashCode()
223 if (lastDisappearHash.equals(newHash)) {
224 return
225 }
226 field = value
227 lastDisappearHash = newHash
228 changedListener?.invoke()
229 }
230
231 private var lastDisappearHash = disappearParameters.hashCode()
232
233 /**
234 * A listener for all changes. This won't be copied over when invoking [copy]
235 */
236 var changedListener: (() -> Unit)? = null
237
238 /**
239 * Get a copy of this state. This won't copy any listeners it may have set
240 */
241 override fun copy(): MediaHostState {
242 val mediaHostState = MediaHostStateHolder()
243 mediaHostState.expansion = expansion
244 mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
245 mediaHostState.measurementInput = measurementInput?.copy()
246 mediaHostState.visible = visible
247 mediaHostState.disappearParameters = disappearParameters.deepCopy()
248 mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
249 return mediaHostState
250 }
251
252 override fun equals(other: Any?): Boolean {
253 if (!(other is MediaHostState)) {
254 return false
255 }
256 if (!Objects.equals(measurementInput, other.measurementInput)) {
257 return false
258 }
259 if (expansion != other.expansion) {
260 return false
261 }
262 if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
263 return false
264 }
265 if (visible != other.visible) {
266 return false
267 }
268 if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
269 return false
270 }
271 if (!disappearParameters.equals(other.disappearParameters)) {
272 return false
273 }
274 return true
275 }
276
277 override fun hashCode(): Int {
278 var result = measurementInput?.hashCode() ?: 0
279 result = 31 * result + expansion.hashCode()
280 result = 31 * result + falsingProtectionNeeded.hashCode()
281 result = 31 * result + showsOnlyActiveMedia.hashCode()
282 result = 31 * result + if (visible) 1 else 2
283 result = 31 * result + disappearParameters.hashCode()
284 return result
285 }
286 }
287 }
288
289 /**
290 * A description of a media host state that describes the behavior whenever the media carousel
291 * is hosted. The HostState notifies the media players of changes to their properties, who
292 * in turn will create view states from it.
293 * When adding a new property to this, make sure to update the listener and notify them
294 * about the changes.
295 * In case you need to have a different rendering based on the state, you can add a new
296 * constraintState to the [MediaViewController]. Otherwise, similar host states will resolve
297 * to the same viewstate, a behavior that is described in [CacheKey]. Make sure to only update
298 * that key if the underlying view needs to have a different measurement.
299 */
300 interface MediaHostState {
301
302 companion object {
303 const val EXPANDED: Float = 1.0f
304 const val COLLAPSED: Float = 0.0f
305 }
306
307 /**
308 * The last measurement input that this state was measured with. Infers width and height of
309 * the players.
310 */
311 var measurementInput: MeasurementInput?
312
313 /**
314 * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions),
315 * [EXPANDED] for fully expanded (up to 5 actions).
316 */
317 var expansion: Float
318
319 /**
320 * Is this host only showing active media or is it showing all of them including resumption?
321 */
322 var showsOnlyActiveMedia: Boolean
323
324 /**
325 * If the view should be VISIBLE or GONE.
326 */
327 val visible: Boolean
328
329 /**
330 * Does this host need any falsing protection?
331 */
332 var falsingProtectionNeeded: Boolean
333
334 /**
335 * The parameters how the view disappears from this location when going to a host that's not
336 * visible. If modified, make sure to set this value again on the host to ensure the values
337 * are propagated
338 */
339 var disappearParameters: DisappearParameters
340
341 /**
342 * Get a copy of this view state, deepcopying all appropriate members
343 */
copynull344 fun copy(): MediaHostState
345 }
346