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