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 }