xref: /aosp_15_r20/frameworks/base/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
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.view
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.Flags.mediaControlsUmoInflationInBackground
24 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
25 import com.android.systemui.media.controls.shared.model.MediaData
26 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
27 import com.android.systemui.media.controls.ui.controller.MediaCarouselController
28 import com.android.systemui.media.controls.ui.controller.MediaCarouselControllerLogger
29 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
30 import com.android.systemui.media.controls.ui.controller.MediaHostStatesManager
31 import com.android.systemui.media.controls.ui.controller.MediaLocation
32 import com.android.systemui.util.animation.DisappearParameters
33 import com.android.systemui.util.animation.MeasurementInput
34 import com.android.systemui.util.animation.MeasurementOutput
35 import com.android.systemui.util.animation.UniqueObjectHostView
36 import java.util.Objects
37 import javax.inject.Inject
38 
39 class MediaHost(
40     private val state: MediaHostStateHolder,
41     private val mediaHierarchyManager: MediaHierarchyManager,
42     private val mediaDataManager: MediaDataManager,
43     private val mediaHostStatesManager: MediaHostStatesManager,
44     private val mediaCarouselController: MediaCarouselController,
45     private val debugLogger: MediaCarouselControllerLogger,
<lambda>null46 ) : MediaHostState by state {
47     lateinit var hostView: UniqueObjectHostView
48     var location: Int = -1
49         private set
50 
51     private var visibleChangedListeners: ArraySet<(Boolean) -> Unit> = ArraySet()
52 
53     private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
54 
55     private var inited: Boolean = false
56 
57     /** Are we listening to media data changes? */
58     private var listeningToMediaData = false
59 
60     /** Get the current bounds on the screen. This makes sure the state is fresh and up to date */
61     val currentBounds: Rect = Rect()
62         get() {
63             hostView.getLocationOnScreen(tmpLocationOnScreen)
64             var left = tmpLocationOnScreen[0] + hostView.paddingLeft
65             var top = tmpLocationOnScreen[1] + hostView.paddingTop
66             var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight
67             var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom
68             // Handle cases when the width or height is 0 but it has padding. In those cases
69             // the above could return negative widths, which is wrong
70             if (right < left) {
71                 left = 0
72                 right = 0
73             }
74             if (bottom < top) {
75                 bottom = 0
76                 top = 0
77             }
78             field.set(left, top, right, bottom)
79             return field
80         }
81 
82     /**
83      * Set the clipping that this host should use, based on its parent's bounds.
84      *
85      * Use [Rect.set].
86      */
87     val currentClipping = Rect()
88 
89     private val listener =
90         object : MediaDataManager.Listener {
91             override fun onMediaDataLoaded(
92                 key: String,
93                 oldKey: String?,
94                 data: MediaData,
95                 immediately: Boolean,
96                 receivedSmartspaceCardLatency: Int,
97                 isSsReactivated: Boolean,
98             ) {
99                 if (mediaControlsUmoInflationInBackground()) return
100 
101                 if (immediately) {
102                     updateViewVisibility()
103                 }
104             }
105 
106             override fun onSmartspaceMediaDataLoaded(
107                 key: String,
108                 data: SmartspaceMediaData,
109                 shouldPrioritize: Boolean,
110             ) {
111                 updateViewVisibility()
112             }
113 
114             override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
115                 updateViewVisibility()
116             }
117 
118             override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
119                 if (immediately) {
120                     updateViewVisibility()
121                 }
122             }
123         }
124 
125     fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
126         visibleChangedListeners.add(listener)
127     }
128 
129     fun removeVisibilityChangeListener(listener: (Boolean) -> Unit) {
130         visibleChangedListeners.remove(listener)
131     }
132 
133     /**
134      * Initialize this MediaObject and create a host view. All state should already be set on this
135      * host before calling this method in order to avoid unnecessary state changes which lead to
136      * remeasurings later on.
137      *
138      * @param location the location this host name has. Used to identify the host during
139      *
140      * ```
141      *                 transitions.
142      * ```
143      */
144     fun init(@MediaLocation location: Int) {
145         if (inited) {
146             return
147         }
148         inited = true
149 
150         this.location = location
151         hostView = mediaHierarchyManager.register(this)
152         // Listen by default, as the host might not be attached by our clients, until
153         // they get a visibility change. We still want to stay up to date in that case!
154         setListeningToMediaData(true)
155         hostView.addOnAttachStateChangeListener(
156             object : OnAttachStateChangeListener {
157                 override fun onViewAttachedToWindow(v: View) {
158                     setListeningToMediaData(true)
159                     updateViewVisibility()
160                 }
161 
162                 override fun onViewDetachedFromWindow(v: View) {
163                     setListeningToMediaData(false)
164                 }
165             }
166         )
167 
168         // Listen to measurement updates and update our state with it
169         hostView.measurementManager =
170             object : UniqueObjectHostView.MeasurementManager {
171                 override fun onMeasure(input: MeasurementInput): MeasurementOutput {
172                     // Modify the measurement to exactly match the dimensions
173                     if (
174                         View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST
175                     ) {
176                         input.widthMeasureSpec =
177                             View.MeasureSpec.makeMeasureSpec(
178                                 View.MeasureSpec.getSize(input.widthMeasureSpec),
179                                 View.MeasureSpec.EXACTLY,
180                             )
181                     }
182                     // This will trigger a state change that ensures that we now have a state
183                     // available
184                     state.measurementInput = input
185                     return mediaHostStatesManager.updateCarouselDimensions(location, state)
186                 }
187             }
188 
189         // Whenever the state changes, let our state manager know
190         state.changedListener = { mediaHostStatesManager.updateHostState(location, state) }
191 
192         updateViewVisibility()
193     }
194 
195     private fun setListeningToMediaData(listen: Boolean) {
196         if (listen != listeningToMediaData) {
197             listeningToMediaData = listen
198             if (listen) {
199                 mediaDataManager.addListener(listener)
200             } else {
201                 mediaDataManager.removeListener(listener)
202             }
203         }
204     }
205 
206     /**
207      * Updates this host's state based on the current media data's status, and invokes listeners if
208      * the visibility has changed
209      */
210     fun updateViewVisibility() {
211         state.visible =
212             if (mediaCarouselController.isLockedAndHidden()) {
213                 false
214             } else if (showsOnlyActiveMedia) {
215                 mediaDataManager.hasActiveMediaOrRecommendation()
216             } else {
217                 mediaDataManager.hasAnyMediaOrRecommendation()
218             }
219         val newVisibility = if (visible) View.VISIBLE else View.GONE
220         if (newVisibility != hostView.visibility) {
221             hostView.visibility = newVisibility
222             debugLogger.logMediaHostVisibility(location, visible)
223             visibleChangedListeners.forEach { it.invoke(visible) }
224         }
225     }
226 
227     class MediaHostStateHolder @Inject constructor() : MediaHostState {
228         override var measurementInput: MeasurementInput? = null
229             set(value) {
230                 if (value?.equals(field) != true) {
231                     field = value
232                     changedListener?.invoke()
233                 }
234             }
235 
236         override var expansion: Float = 0.0f
237             set(value) {
238                 if (!value.equals(field)) {
239                     field = value
240                     changedListener?.invoke()
241                 }
242             }
243 
244         override var expandedMatchesParentHeight: Boolean = false
245             set(value) {
246                 if (value != field) {
247                     field = value
248                     changedListener?.invoke()
249                 }
250             }
251 
252         override var squishFraction: Float = 1.0f
253             set(value) {
254                 if (!value.equals(field)) {
255                     field = value
256                     changedListener?.invoke()
257                 }
258             }
259 
260         override var showsOnlyActiveMedia: Boolean = false
261             set(value) {
262                 if (!value.equals(field)) {
263                     field = value
264                     changedListener?.invoke()
265                 }
266             }
267 
268         override var visible: Boolean = true
269             set(value) {
270                 if (field == value) {
271                     return
272                 }
273                 field = value
274                 changedListener?.invoke()
275             }
276 
277         override var falsingProtectionNeeded: Boolean = false
278             set(value) {
279                 if (field == value) {
280                     return
281                 }
282                 field = value
283                 changedListener?.invoke()
284             }
285 
286         override var disappearParameters: DisappearParameters = DisappearParameters()
287             set(value) {
288                 val newHash = value.hashCode()
289                 if (lastDisappearHash.equals(newHash)) {
290                     return
291                 }
292                 field = value
293                 lastDisappearHash = newHash
294                 changedListener?.invoke()
295             }
296 
297         override var disablePagination: Boolean = false
298             set(value) {
299                 if (field == value) {
300                     return
301                 }
302                 field = value
303                 changedListener?.invoke()
304             }
305 
306         private var lastDisappearHash = disappearParameters.hashCode()
307 
308         /** A listener for all changes. This won't be copied over when invoking [copy] */
309         var changedListener: (() -> Unit)? = null
310 
311         /** Get a copy of this state. This won't copy any listeners it may have set */
312         override fun copy(): MediaHostState {
313             val mediaHostState = MediaHostStateHolder()
314             mediaHostState.expansion = expansion
315             mediaHostState.expandedMatchesParentHeight = expandedMatchesParentHeight
316             mediaHostState.squishFraction = squishFraction
317             mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
318             mediaHostState.measurementInput = measurementInput?.copy()
319             mediaHostState.visible = visible
320             mediaHostState.disappearParameters = disappearParameters.deepCopy()
321             mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
322             mediaHostState.disablePagination = disablePagination
323             return mediaHostState
324         }
325 
326         override fun equals(other: Any?): Boolean {
327             if (!(other is MediaHostState)) {
328                 return false
329             }
330             if (!Objects.equals(measurementInput, other.measurementInput)) {
331                 return false
332             }
333             if (expansion != other.expansion) {
334                 return false
335             }
336             if (squishFraction != other.squishFraction) {
337                 return false
338             }
339             if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
340                 return false
341             }
342             if (visible != other.visible) {
343                 return false
344             }
345             if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
346                 return false
347             }
348             if (!disappearParameters.equals(other.disappearParameters)) {
349                 return false
350             }
351             if (disablePagination != other.disablePagination) {
352                 return false
353             }
354             return true
355         }
356 
357         override fun hashCode(): Int {
358             var result = measurementInput?.hashCode() ?: 0
359             result = 31 * result + expansion.hashCode()
360             result = 31 * result + squishFraction.hashCode()
361             result = 31 * result + falsingProtectionNeeded.hashCode()
362             result = 31 * result + showsOnlyActiveMedia.hashCode()
363             result = 31 * result + if (visible) 1 else 2
364             result = 31 * result + disappearParameters.hashCode()
365             result = 31 * result + disablePagination.hashCode()
366             return result
367         }
368     }
369 }
370 
371 /**
372  * A description of a media host state that describes the behavior whenever the media carousel is
373  * hosted. The HostState notifies the media players of changes to their properties, who in turn will
374  * create view states from it. When adding a new property to this, make sure to update the listener
375  * and notify them about the changes. In case you need to have a different rendering based on the
376  * state, you can add a new constraintState to the [MediaViewController]. Otherwise, similar host
377  * states will resolve to the same viewstate, a behavior that is described in [CacheKey]. Make sure
378  * to only update that key if the underlying view needs to have a different measurement.
379  */
380 interface MediaHostState {
381 
382     companion object {
383         const val EXPANDED: Float = 1.0f
384         const val COLLAPSED: Float = 0.0f
385     }
386 
387     /**
388      * The last measurement input that this state was measured with. Infers width and height of the
389      * players.
390      */
391     var measurementInput: MeasurementInput?
392 
393     /**
394      * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), [EXPANDED]
395      * for fully expanded (up to 5 actions).
396      */
397     var expansion: Float
398 
399     /**
400      * If true, the [EXPANDED] layout should stretch to match the height of its parent container,
401      * rather than having a fixed height.
402      */
403     var expandedMatchesParentHeight: Boolean
404 
405     /** Fraction of the height animation. */
406     var squishFraction: Float
407 
408     /** Is this host only showing active media or is it showing all of them including resumption? */
409     var showsOnlyActiveMedia: Boolean
410 
411     /** If the view should be VISIBLE or GONE. */
412     val visible: Boolean
413 
414     /** Does this host need any falsing protection? */
415     var falsingProtectionNeeded: Boolean
416 
417     /**
418      * The parameters how the view disappears from this location when going to a host that's not
419      * visible. If modified, make sure to set this value again on the host to ensure the values are
420      * propagated
421      */
422     var disappearParameters: DisappearParameters
423 
424     /**
425      * Whether pagination should be disabled for this host, meaning that when there are multiple
426      * media sessions, only the first one will appear.
427      */
428     var disablePagination: Boolean
429 
430     /** Get a copy of this view state, deepcopying all appropriate members */
copynull431     fun copy(): MediaHostState
432 }
433