Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 1 | package com.android.systemui.media |
| 2 | |
Lucas Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 3 | import android.graphics.PointF |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 4 | import android.graphics.Rect |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 5 | import android.view.View |
| 6 | import android.view.View.OnAttachStateChangeListener |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 7 | import com.android.systemui.util.animation.MeasurementInput |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 8 | import com.android.systemui.util.animation.MeasurementOutput |
| 9 | import com.android.systemui.util.animation.UniqueObjectHostView |
| 10 | import java.util.Objects |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 11 | import javax.inject.Inject |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 12 | |
| 13 | class MediaHost @Inject constructor( |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 14 | private val state: MediaHostStateHolder, |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 15 | private val mediaHierarchyManager: MediaHierarchyManager, |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 16 | private val mediaDataManager: MediaDataManager, |
| 17 | private val mediaDataManagerCombineLatest: MediaDataCombineLatest, |
| 18 | private val mediaHostStatesManager: MediaHostStatesManager |
| 19 | ) : MediaHostState by state { |
| 20 | lateinit var hostView: UniqueObjectHostView |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 21 | var location: Int = -1 |
| 22 | private set |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 23 | var visibleChangedListener: ((Boolean) -> Unit)? = null |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 24 | |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 25 | private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0) |
| 26 | |
| 27 | /** |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 28 | * Get the current bounds on the screen. This makes sure the state is fresh and up to date |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 29 | */ |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 30 | val currentBounds: Rect = Rect() |
Lucas Dupin | 5b27cbc | 2020-05-18 10:46:50 -0700 | [diff] [blame] | 31 | get() { |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 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 |
Lucas Dupin | 5b27cbc | 2020-05-18 10:46:50 -0700 | [diff] [blame] | 41 | right = 0 |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 42 | } |
| 43 | if (bottom < top) { |
| 44 | bottom = 0 |
Lucas Dupin | 5b27cbc | 2020-05-18 10:46:50 -0700 | [diff] [blame] | 45 | top = 0 |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 46 | } |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 47 | field.set(left, top, right, bottom) |
| 48 | return field |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 49 | } |
| 50 | |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 51 | private val listener = object : MediaDataManager.Listener { |
Beth Thibodeau | f55bc6a | 2020-05-20 02:01:31 -0400 | [diff] [blame] | 52 | override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 53 | updateViewVisibility() |
| 54 | } |
| 55 | |
| 56 | override fun onMediaDataRemoved(key: String) { |
| 57 | updateViewVisibility() |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | /** |
Selim Cinek | 2d7be5f | 2020-05-01 13:16:01 -0700 | [diff] [blame] | 62 | * Initialize this MediaObject and create a host view. |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 63 | * 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 Cinek | 2d7be5f | 2020-05-01 13:16:01 -0700 | [diff] [blame] | 65 | * |
| 66 | * @param location the location this host name has. Used to identify the host during |
| 67 | * transitions. |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 68 | */ |
| 69 | fun init(@MediaLocation location: Int) { |
Lucas Dupin | 5b27cbc | 2020-05-18 10:46:50 -0700 | [diff] [blame] | 70 | this.location = location |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 71 | hostView = mediaHierarchyManager.register(this) |
| 72 | hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { |
| 73 | override fun onViewAttachedToWindow(v: View?) { |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 74 | // 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 Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 79 | updateViewVisibility() |
| 80 | } |
| 81 | |
| 82 | override fun onViewDetachedFromWindow(v: View?) { |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 83 | mediaDataManagerCombineLatest.removeListener(listener) |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 84 | } |
| 85 | }) |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 86 | |
| 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 Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 107 | updateViewVisibility() |
| 108 | } |
| 109 | |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 110 | private fun updateViewVisibility() { |
Lucas Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 111 | visible = if (showsOnlyActiveMedia) { |
| 112 | mediaDataManager.hasActiveMedia() |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 113 | } else { |
Lucas Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 114 | mediaDataManager.hasAnyMedia() |
Selim Cinek | b52642b | 2020-04-17 14:30:29 -0700 | [diff] [blame] | 115 | } |
| 116 | hostView.visibility = if (visible) View.VISIBLE else View.GONE |
| 117 | visibleChangedListener?.invoke(visible) |
| 118 | } |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 119 | |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 120 | class MediaHostStateHolder @Inject constructor() : MediaHostState { |
Lucas Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 121 | private var gonePivot: PointF = PointF() |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 122 | |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 123 | 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 Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 147 | 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 Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 166 | /** |
| 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 Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 176 | mediaHostState.expansion = expansion |
| 177 | mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 178 | mediaHostState.measurementInput = measurementInput?.copy() |
Lucas Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 179 | mediaHostState.visible = visible |
| 180 | mediaHostState.gonePivot.set(gonePivot) |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 181 | return mediaHostState |
| 182 | } |
| 183 | |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 184 | override fun equals(other: Any?): Boolean { |
| 185 | if (!(other is MediaHostState)) { |
| 186 | return false |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 187 | } |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 188 | 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 Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 197 | if (visible != other.visible) { |
| 198 | return false |
| 199 | } |
| 200 | if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) { |
| 201 | return false |
| 202 | } |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 203 | 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 Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 210 | result = 31 * result + if (visible) 1 else 2 |
| 211 | result = 31 * result + gonePivot.hashCode() |
Lucas Dupin | 5b27cbc | 2020-05-18 10:46:50 -0700 | [diff] [blame] | 212 | return result |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 213 | } |
| 214 | } |
| 215 | } |
| 216 | |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 217 | interface 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 Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 226 | * The expansion of the player, 0 for fully collapsed (up to 3 actions), 1 for fully expanded |
| 227 | * (up to 5 actions.) |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 228 | */ |
Selim Cinek | f0f7495 | 2020-04-21 11:45:16 -0700 | [diff] [blame] | 229 | var expansion: Float |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 230 | |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 231 | /** |
| 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 Dupin | 84f5a0e | 2020-06-08 19:55:33 -0700 | [diff] [blame] | 237 | * 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 Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 261 | * Get a copy of this view state, deepcopying all appropriate members |
| 262 | */ |
| 263 | fun copy(): MediaHostState |
Lucas Dupin | 5b27cbc | 2020-05-18 10:46:50 -0700 | [diff] [blame] | 264 | } |