• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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