• 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.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         val oldState = state.visible
212         state.visible =
213             if (mediaCarouselController.isLockedAndHidden()) {
214                 false
215             } else if (showsOnlyActiveMedia) {
216                 mediaDataManager.hasActiveMediaOrRecommendation()
217             } else {
218                 mediaDataManager.hasAnyMediaOrRecommendation()
219             }
220         val newVisibility = if (visible) View.VISIBLE else View.GONE
221         if (oldState != state.visible || newVisibility != hostView.visibility) {
222             hostView.visibility = newVisibility
223             debugLogger.logMediaHostVisibility(location, visible, oldState)
224             visibleChangedListeners.forEach { it.invoke(visible) }
225         }
226     }
227 
228     class MediaHostStateHolder @Inject constructor() : MediaHostState {
229         override var measurementInput: MeasurementInput? = null
230             set(value) {
231                 if (value?.equals(field) != true) {
232                     field = value
233                     changedListener?.invoke()
234                 }
235             }
236 
237         override var expansion: Float = 0.0f
238             set(value) {
239                 if (!value.equals(field)) {
240                     field = value
241                     changedListener?.invoke()
242                 }
243             }
244 
245         override var expandedMatchesParentHeight: Boolean = false
246             set(value) {
247                 if (value != field) {
248                     field = value
249                     changedListener?.invoke()
250                 }
251             }
252 
253         override var squishFraction: Float = 1.0f
254             set(value) {
255                 if (!value.equals(field)) {
256                     field = value
257                     changedListener?.invoke()
258                 }
259             }
260 
261         override var showsOnlyActiveMedia: Boolean = false
262             set(value) {
263                 if (!value.equals(field)) {
264                     field = value
265                     changedListener?.invoke()
266                 }
267             }
268 
269         override var visible: Boolean = true
270             set(value) {
271                 if (field == value) {
272                     return
273                 }
274                 field = value
275                 changedListener?.invoke()
276             }
277 
278         override var falsingProtectionNeeded: Boolean = false
279             set(value) {
280                 if (field == value) {
281                     return
282                 }
283                 field = value
284                 changedListener?.invoke()
285             }
286 
287         override var disappearParameters: DisappearParameters = DisappearParameters()
288             set(value) {
289                 val newHash = value.hashCode()
290                 if (lastDisappearHash.equals(newHash)) {
291                     return
292                 }
293                 field = value
294                 lastDisappearHash = newHash
295                 changedListener?.invoke()
296             }
297 
298         override var disableScrolling: Boolean = false
299             set(value) {
300                 if (field == value) {
301                     return
302                 }
303                 field = value
304                 changedListener?.invoke()
305             }
306 
307         private var lastDisappearHash = disappearParameters.hashCode()
308 
309         /** A listener for all changes. This won't be copied over when invoking [copy] */
310         var changedListener: (() -> Unit)? = null
311 
312         /** Get a copy of this state. This won't copy any listeners it may have set */
313         override fun copy(): MediaHostState {
314             val mediaHostState = MediaHostStateHolder()
315             mediaHostState.expansion = expansion
316             mediaHostState.expandedMatchesParentHeight = expandedMatchesParentHeight
317             mediaHostState.squishFraction = squishFraction
318             mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
319             mediaHostState.measurementInput = measurementInput?.copy()
320             mediaHostState.visible = visible
321             mediaHostState.disappearParameters = disappearParameters.deepCopy()
322             mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
323             mediaHostState.disableScrolling = disableScrolling
324             return mediaHostState
325         }
326 
327         override fun equals(other: Any?): Boolean {
328             if (!(other is MediaHostState)) {
329                 return false
330             }
331             if (!Objects.equals(measurementInput, other.measurementInput)) {
332                 return false
333             }
334             if (expansion != other.expansion) {
335                 return false
336             }
337             if (squishFraction != other.squishFraction) {
338                 return false
339             }
340             if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
341                 return false
342             }
343             if (visible != other.visible) {
344                 return false
345             }
346             if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
347                 return false
348             }
349             if (!disappearParameters.equals(other.disappearParameters)) {
350                 return false
351             }
352             if (disableScrolling != other.disableScrolling) {
353                 return false
354             }
355             return true
356         }
357 
358         override fun hashCode(): Int {
359             var result = measurementInput?.hashCode() ?: 0
360             result = 31 * result + expansion.hashCode()
361             result = 31 * result + squishFraction.hashCode()
362             result = 31 * result + falsingProtectionNeeded.hashCode()
363             result = 31 * result + showsOnlyActiveMedia.hashCode()
364             result = 31 * result + if (visible) 1 else 2
365             result = 31 * result + disappearParameters.hashCode()
366             result = 31 * result + disableScrolling.hashCode()
367             return result
368         }
369     }
370 }
371 
372 /**
373  * A description of a media host state that describes the behavior whenever the media carousel is
374  * hosted. The HostState notifies the media players of changes to their properties, who in turn will
375  * create view states from it. When adding a new property to this, make sure to update the listener
376  * and notify them about the changes. In case you need to have a different rendering based on the
377  * state, you can add a new constraintState to the [MediaViewController]. Otherwise, similar host
378  * states will resolve to the same viewstate, a behavior that is described in [CacheKey]. Make sure
379  * to only update that key if the underlying view needs to have a different measurement.
380  */
381 interface MediaHostState {
382 
383     companion object {
384         const val EXPANDED: Float = 1.0f
385         const val COLLAPSED: Float = 0.0f
386     }
387 
388     /**
389      * The last measurement input that this state was measured with. Infers width and height of the
390      * players.
391      */
392     var measurementInput: MeasurementInput?
393 
394     /**
395      * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), [EXPANDED]
396      * for fully expanded (up to 5 actions).
397      */
398     var expansion: Float
399 
400     /**
401      * If true, the [EXPANDED] layout should stretch to match the height of its parent container,
402      * rather than having a fixed height.
403      */
404     var expandedMatchesParentHeight: Boolean
405 
406     /** Fraction of the height animation. */
407     var squishFraction: Float
408 
409     /** Is this host only showing active media or is it showing all of them including resumption? */
410     var showsOnlyActiveMedia: Boolean
411 
412     /** If the view should be VISIBLE or GONE. */
413     val visible: Boolean
414 
415     /** Does this host need any falsing protection? */
416     var falsingProtectionNeeded: Boolean
417 
418     /**
419      * The parameters how the view disappears from this location when going to a host that's not
420      * visible. If modified, make sure to set this value again on the host to ensure the values are
421      * propagated
422      */
423     var disappearParameters: DisappearParameters
424 
425     /**
426      * Whether scrolling should be disabled for this host, meaning that when there are multiple
427      * media sessions, it will not be possible to scroll between media sessions or swipe away the
428      * entire media carousel. The first media session will always be shown.
429      */
430     var disableScrolling: Boolean
431 
432     /** Get a copy of this view state, deepcopying all appropriate members */
copynull433     fun copy(): MediaHostState
434 }
435