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