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