blob: 1ae9d3ff4ca536d376a5bd751e0a5e5093248ff0 [file] [log] [blame]
package com.android.systemui.media
import android.graphics.PointF
import android.graphics.Rect
import android.util.ArraySet
import android.view.View
import android.view.View.OnAttachStateChangeListener
import com.android.systemui.util.animation.MeasurementInput
import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.UniqueObjectHostView
import java.util.Objects
import javax.inject.Inject
class MediaHost @Inject constructor(
private val state: MediaHostStateHolder,
private val mediaHierarchyManager: MediaHierarchyManager,
private val mediaDataManager: MediaDataManager,
private val mediaDataManagerCombineLatest: MediaDataCombineLatest,
private val mediaHostStatesManager: MediaHostStatesManager
) : MediaHostState by state {
lateinit var hostView: UniqueObjectHostView
var location: Int = -1
private set
private var visibleChangedListeners: ArraySet<(Boolean) -> Unit> = ArraySet()
private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
/**
* Get the current bounds on the screen. This makes sure the state is fresh and up to date
*/
val currentBounds: Rect = Rect()
get() {
hostView.getLocationOnScreen(tmpLocationOnScreen)
var left = tmpLocationOnScreen[0] + hostView.paddingLeft
var top = tmpLocationOnScreen[1] + hostView.paddingTop
var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight
var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom
// Handle cases when the width or height is 0 but it has padding. In those cases
// the above could return negative widths, which is wrong
if (right < left) {
left = 0
right = 0
}
if (bottom < top) {
bottom = 0
top = 0
}
field.set(left, top, right, bottom)
return field
}
private val listener = object : MediaDataManager.Listener {
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
updateViewVisibility()
}
override fun onMediaDataRemoved(key: String) {
updateViewVisibility()
}
}
fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
visibleChangedListeners.add(listener)
}
/**
* Initialize this MediaObject and create a host view.
* All state should already be set on this host before calling this method in order to avoid
* unnecessary state changes which lead to remeasurings later on.
*
* @param location the location this host name has. Used to identify the host during
* transitions.
*/
fun init(@MediaLocation location: Int) {
this.location = location
hostView = mediaHierarchyManager.register(this)
hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
// we should listen to the combined state change, since otherwise there might
// be a delay until the views and the controllers are initialized, leaving us
// with either a blank view or the controllers not yet initialized and the
// measuring wrong
mediaDataManagerCombineLatest.addListener(listener)
updateViewVisibility()
}
override fun onViewDetachedFromWindow(v: View?) {
mediaDataManagerCombineLatest.removeListener(listener)
}
})
// Listen to measurement updates and update our state with it
hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager {
override fun onMeasure(input: MeasurementInput): MeasurementOutput {
// Modify the measurement to exactly match the dimensions
if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) {
input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
View.MeasureSpec.getSize(input.widthMeasureSpec),
View.MeasureSpec.EXACTLY)
}
// This will trigger a state change that ensures that we now have a state available
state.measurementInput = input
return mediaHostStatesManager.getPlayerDimensions(state)
}
}
// Whenever the state changes, let our state manager know
state.changedListener = {
mediaHostStatesManager.updateHostState(location, state)
}
updateViewVisibility()
}
private fun updateViewVisibility() {
visible = if (showsOnlyActiveMedia) {
mediaDataManager.hasActiveMedia()
} else {
mediaDataManager.hasAnyMedia()
}
val newVisibility = if (visible) View.VISIBLE else View.GONE
if (newVisibility != hostView.visibility) {
hostView.visibility = newVisibility
visibleChangedListeners.forEach {
it.invoke(visible)
}
}
}
class MediaHostStateHolder @Inject constructor() : MediaHostState {
private var gonePivot: PointF = PointF()
override var measurementInput: MeasurementInput? = null
set(value) {
if (value?.equals(field) != true) {
field = value
changedListener?.invoke()
}
}
override var expansion: Float = 0.0f
set(value) {
if (!value.equals(field)) {
field = value
changedListener?.invoke()
}
}
override var showsOnlyActiveMedia: Boolean = false
set(value) {
if (!value.equals(field)) {
field = value
changedListener?.invoke()
}
}
override var visible: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
changedListener?.invoke()
}
override var falsingProtectionNeeded: Boolean = false
set(value) {
if (field == value) {
return
}
field = value
changedListener?.invoke()
}
override fun getPivotX(): Float = gonePivot.x
override fun getPivotY(): Float = gonePivot.y
override fun setGonePivot(x: Float, y: Float) {
if (gonePivot.equals(x, y)) {
return
}
gonePivot.set(x, y)
changedListener?.invoke()
}
/**
* A listener for all changes. This won't be copied over when invoking [copy]
*/
var changedListener: (() -> Unit)? = null
/**
* Get a copy of this state. This won't copy any listeners it may have set
*/
override fun copy(): MediaHostState {
val mediaHostState = MediaHostStateHolder()
mediaHostState.expansion = expansion
mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
mediaHostState.measurementInput = measurementInput?.copy()
mediaHostState.visible = visible
mediaHostState.gonePivot.set(gonePivot)
mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
return mediaHostState
}
override fun equals(other: Any?): Boolean {
if (!(other is MediaHostState)) {
return false
}
if (!Objects.equals(measurementInput, other.measurementInput)) {
return false
}
if (expansion != other.expansion) {
return false
}
if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
return false
}
if (visible != other.visible) {
return false
}
if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
return false
}
if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
return false
}
return true
}
override fun hashCode(): Int {
var result = measurementInput?.hashCode() ?: 0
result = 31 * result + expansion.hashCode()
result = 31 * result + falsingProtectionNeeded.hashCode()
result = 31 * result + showsOnlyActiveMedia.hashCode()
result = 31 * result + if (visible) 1 else 2
result = 31 * result + gonePivot.hashCode()
return result
}
}
}
interface MediaHostState {
/**
* The last measurement input that this state was measured with. Infers with and height of
* the players.
*/
var measurementInput: MeasurementInput?
/**
* The expansion of the player, 0 for fully collapsed (up to 3 actions), 1 for fully expanded
* (up to 5 actions.)
*/
var expansion: Float
/**
* Is this host only showing active media or is it showing all of them including resumption?
*/
var showsOnlyActiveMedia: Boolean
/**
* If the view should be VISIBLE or GONE.
*/
var visible: Boolean
/**
* Does this host need any falsing protection?
*/
var falsingProtectionNeeded: Boolean
/**
* Sets the pivot point when clipping the height or width.
* Clipping happens when animating visibility when we're visible in QS but not on QQS,
* for example.
*/
fun setGonePivot(x: Float, y: Float)
/**
* x position of pivot, from 0 to 1
* @see [setGonePivot]
*/
fun getPivotX(): Float
/**
* y position of pivot, from 0 to 1
* @see [setGonePivot]
*/
fun getPivotY(): Float
/**
* Get a copy of this view state, deepcopying all appropriate members
*/
fun copy(): MediaHostState
}