blob: 7c5f0d1c2a16b55b60108b28a592275e7ff976b1 [file] [log] [blame]
Selim Cinekb52642b2020-04-17 14:30:29 -07001package com.android.systemui.media
2
Lucas Dupin84f5a0e2020-06-08 19:55:33 -07003import android.graphics.PointF
Selim Cinekf0f74952020-04-21 11:45:16 -07004import android.graphics.Rect
Selim Cinekb52642b2020-04-17 14:30:29 -07005import android.view.View
6import android.view.View.OnAttachStateChangeListener
Selim Cinek54809622020-04-30 19:04:44 -07007import com.android.systemui.util.animation.MeasurementInput
Selim Cinek2de5ebb2020-05-20 15:39:03 -07008import com.android.systemui.util.animation.MeasurementOutput
9import com.android.systemui.util.animation.UniqueObjectHostView
10import java.util.Objects
Selim Cinekb52642b2020-04-17 14:30:29 -070011import javax.inject.Inject
Selim Cinekb52642b2020-04-17 14:30:29 -070012
13class MediaHost @Inject constructor(
Selim Cinek2de5ebb2020-05-20 15:39:03 -070014 private val state: MediaHostStateHolder,
Selim Cinekb52642b2020-04-17 14:30:29 -070015 private val mediaHierarchyManager: MediaHierarchyManager,
Selim Cinek2de5ebb2020-05-20 15:39:03 -070016 private val mediaDataManager: MediaDataManager,
17 private val mediaDataManagerCombineLatest: MediaDataCombineLatest,
18 private val mediaHostStatesManager: MediaHostStatesManager
19) : MediaHostState by state {
20 lateinit var hostView: UniqueObjectHostView
Selim Cinekb52642b2020-04-17 14:30:29 -070021 var location: Int = -1
22 private set
Selim Cinekb52642b2020-04-17 14:30:29 -070023 var visibleChangedListener: ((Boolean) -> Unit)? = null
Selim Cinekb52642b2020-04-17 14:30:29 -070024
Selim Cinekf0f74952020-04-21 11:45:16 -070025 private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
26
27 /**
Selim Cinek2de5ebb2020-05-20 15:39:03 -070028 * Get the current bounds on the screen. This makes sure the state is fresh and up to date
Selim Cinekf0f74952020-04-21 11:45:16 -070029 */
Selim Cinek2de5ebb2020-05-20 15:39:03 -070030 val currentBounds: Rect = Rect()
Lucas Dupin5b27cbc2020-05-18 10:46:50 -070031 get() {
Selim Cinekf0f74952020-04-21 11:45:16 -070032 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
Lucas Dupin5b27cbc2020-05-18 10:46:50 -070041 right = 0
Selim Cinekf0f74952020-04-21 11:45:16 -070042 }
43 if (bottom < top) {
44 bottom = 0
Lucas Dupin5b27cbc2020-05-18 10:46:50 -070045 top = 0
Selim Cinekf0f74952020-04-21 11:45:16 -070046 }
Selim Cinek2de5ebb2020-05-20 15:39:03 -070047 field.set(left, top, right, bottom)
48 return field
Selim Cinekf0f74952020-04-21 11:45:16 -070049 }
50
Selim Cinekb52642b2020-04-17 14:30:29 -070051 private val listener = object : MediaDataManager.Listener {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040052 override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
Selim Cinekb52642b2020-04-17 14:30:29 -070053 updateViewVisibility()
54 }
55
56 override fun onMediaDataRemoved(key: String) {
57 updateViewVisibility()
58 }
59 }
60
61 /**
Selim Cinek2d7be5f2020-05-01 13:16:01 -070062 * Initialize this MediaObject and create a host view.
Selim Cinek2de5ebb2020-05-20 15:39:03 -070063 * All state should already be set on this host before calling this method in order to avoid
64 * unnecessary state changes which lead to remeasurings later on.
Selim Cinek2d7be5f2020-05-01 13:16:01 -070065 *
66 * @param location the location this host name has. Used to identify the host during
67 * transitions.
Selim Cinekb52642b2020-04-17 14:30:29 -070068 */
69 fun init(@MediaLocation location: Int) {
Lucas Dupin5b27cbc2020-05-18 10:46:50 -070070 this.location = location
Selim Cinekb52642b2020-04-17 14:30:29 -070071 hostView = mediaHierarchyManager.register(this)
72 hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
73 override fun onViewAttachedToWindow(v: View?) {
Selim Cinek2de5ebb2020-05-20 15:39:03 -070074 // we should listen to the combined state change, since otherwise there might
75 // be a delay until the views and the controllers are initialized, leaving us
76 // with either a blank view or the controllers not yet initialized and the
77 // measuring wrong
78 mediaDataManagerCombineLatest.addListener(listener)
Selim Cinekb52642b2020-04-17 14:30:29 -070079 updateViewVisibility()
80 }
81
82 override fun onViewDetachedFromWindow(v: View?) {
Selim Cinek2de5ebb2020-05-20 15:39:03 -070083 mediaDataManagerCombineLatest.removeListener(listener)
Selim Cinekb52642b2020-04-17 14:30:29 -070084 }
85 })
Selim Cinek2de5ebb2020-05-20 15:39:03 -070086
87 // Listen to measurement updates and update our state with it
88 hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager {
89 override fun onMeasure(input: MeasurementInput): MeasurementOutput {
90 // Modify the measurement to exactly match the dimensions
91 if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) {
92 input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
93 View.MeasureSpec.getSize(input.widthMeasureSpec),
94 View.MeasureSpec.EXACTLY)
95 }
96 // This will trigger a state change that ensures that we now have a state available
97 state.measurementInput = input
98 return mediaHostStatesManager.getPlayerDimensions(state)
99 }
100 }
101
102 // Whenever the state changes, let our state manager know
103 state.changedListener = {
104 mediaHostStatesManager.updateHostState(location, state)
105 }
106
Selim Cinekb52642b2020-04-17 14:30:29 -0700107 updateViewVisibility()
108 }
109
Selim Cinekb52642b2020-04-17 14:30:29 -0700110 private fun updateViewVisibility() {
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700111 visible = if (showsOnlyActiveMedia) {
112 mediaDataManager.hasActiveMedia()
Selim Cinekb52642b2020-04-17 14:30:29 -0700113 } else {
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700114 mediaDataManager.hasAnyMedia()
Selim Cinekb52642b2020-04-17 14:30:29 -0700115 }
116 hostView.visibility = if (visible) View.VISIBLE else View.GONE
117 visibleChangedListener?.invoke(visible)
118 }
Selim Cinekf0f74952020-04-21 11:45:16 -0700119
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700120 class MediaHostStateHolder @Inject constructor() : MediaHostState {
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700121 private var gonePivot: PointF = PointF()
Selim Cinekf0f74952020-04-21 11:45:16 -0700122
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700123 override var measurementInput: MeasurementInput? = null
124 set(value) {
125 if (value?.equals(field) != true) {
126 field = value
127 changedListener?.invoke()
128 }
129 }
130
131 override var expansion: Float = 0.0f
132 set(value) {
133 if (!value.equals(field)) {
134 field = value
135 changedListener?.invoke()
136 }
137 }
138
139 override var showsOnlyActiveMedia: Boolean = false
140 set(value) {
141 if (!value.equals(field)) {
142 field = value
143 changedListener?.invoke()
144 }
145 }
146
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700147 override var visible: Boolean = true
148 set(value) {
149 if (field == value) {
150 return
151 }
152 field = value
153 changedListener?.invoke()
154 }
155
156 override fun getPivotX(): Float = gonePivot.x
157 override fun getPivotY(): Float = gonePivot.y
158 override fun setGonePivot(x: Float, y: Float) {
159 if (gonePivot.equals(x, y)) {
160 return
161 }
162 gonePivot.set(x, y)
163 changedListener?.invoke()
164 }
165
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700166 /**
167 * A listener for all changes. This won't be copied over when invoking [copy]
168 */
169 var changedListener: (() -> Unit)? = null
170
171 /**
172 * Get a copy of this state. This won't copy any listeners it may have set
173 */
174 override fun copy(): MediaHostState {
175 val mediaHostState = MediaHostStateHolder()
Selim Cinekf0f74952020-04-21 11:45:16 -0700176 mediaHostState.expansion = expansion
177 mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700178 mediaHostState.measurementInput = measurementInput?.copy()
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700179 mediaHostState.visible = visible
180 mediaHostState.gonePivot.set(gonePivot)
Selim Cinekf0f74952020-04-21 11:45:16 -0700181 return mediaHostState
182 }
183
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700184 override fun equals(other: Any?): Boolean {
185 if (!(other is MediaHostState)) {
186 return false
Selim Cinek54809622020-04-30 19:04:44 -0700187 }
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700188 if (!Objects.equals(measurementInput, other.measurementInput)) {
189 return false
190 }
191 if (expansion != other.expansion) {
192 return false
193 }
194 if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
195 return false
196 }
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700197 if (visible != other.visible) {
198 return false
199 }
200 if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
201 return false
202 }
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700203 return true
204 }
205
206 override fun hashCode(): Int {
207 var result = measurementInput?.hashCode() ?: 0
208 result = 31 * result + expansion.hashCode()
209 result = 31 * result + showsOnlyActiveMedia.hashCode()
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700210 result = 31 * result + if (visible) 1 else 2
211 result = 31 * result + gonePivot.hashCode()
Lucas Dupin5b27cbc2020-05-18 10:46:50 -0700212 return result
Selim Cinekf0f74952020-04-21 11:45:16 -0700213 }
214 }
215}
216
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700217interface MediaHostState {
218
219 /**
220 * The last measurement input that this state was measured with. Infers with and height of
221 * the players.
222 */
223 var measurementInput: MeasurementInput?
224
225 /**
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700226 * The expansion of the player, 0 for fully collapsed (up to 3 actions), 1 for fully expanded
227 * (up to 5 actions.)
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700228 */
Selim Cinekf0f74952020-04-21 11:45:16 -0700229 var expansion: Float
Selim Cinek54809622020-04-30 19:04:44 -0700230
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700231 /**
232 * Is this host only showing active media or is it showing all of them including resumption?
233 */
234 var showsOnlyActiveMedia: Boolean
235
236 /**
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700237 * If the view should be VISIBLE or GONE.
238 */
239 var visible: Boolean
240
241 /**
242 * Sets the pivot point when clipping the height or width.
243 * Clipping happens when animating visibility when we're visible in QS but not on QQS,
244 * for example.
245 */
246 fun setGonePivot(x: Float, y: Float)
247
248 /**
249 * x position of pivot, from 0 to 1
250 * @see [setGonePivot]
251 */
252 fun getPivotX(): Float
253
254 /**
255 * y position of pivot, from 0 to 1
256 * @see [setGonePivot]
257 */
258 fun getPivotY(): Float
259
260 /**
Selim Cinek2de5ebb2020-05-20 15:39:03 -0700261 * Get a copy of this view state, deepcopying all appropriate members
262 */
263 fun copy(): MediaHostState
Lucas Dupin5b27cbc2020-05-18 10:46:50 -0700264}