| package com.android.systemui.media |
| |
| import android.graphics.Rect |
| 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 |
| var visibleChangedListener: ((Boolean) -> Unit)? = null |
| var visible: Boolean = false |
| private set |
| |
| 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() |
| } |
| } |
| |
| /** |
| * 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() { |
| if (showsOnlyActiveMedia) { |
| visible = mediaDataManager.hasActiveMedia() |
| } else { |
| visible = mediaDataManager.hasAnyMedia() |
| } |
| hostView.visibility = if (visible) View.VISIBLE else View.GONE |
| visibleChangedListener?.invoke(visible) |
| } |
| |
| class MediaHostStateHolder @Inject constructor() : MediaHostState { |
| |
| 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() |
| } |
| } |
| |
| /** |
| * 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() |
| 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 |
| } |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = measurementInput?.hashCode() ?: 0 |
| result = 31 * result + expansion.hashCode() |
| result = 31 * result + showsOnlyActiveMedia.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, 1 for fully expanded |
| */ |
| var expansion: Float |
| |
| /** |
| * Is this host only showing active media or is it showing all of them including resumption? |
| */ |
| var showsOnlyActiveMedia: Boolean |
| |
| /** |
| * Get a copy of this view state, deepcopying all appropriate members |
| */ |
| fun copy(): MediaHostState |
| } |