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