blob: 49d2d8860a2fa6777047314582c6dc86923a00e3 [file] [log] [blame]
package com.android.systemui.media
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.media.InfoMediaManager
import com.android.settingslib.media.LocalMediaManager
import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.notification.VisualStabilityManager
import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.UniqueObjectHostView
import com.android.systemui.util.concurrency.DelayableExecutor
import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Singleton
/**
* Class that is responsible for keeping the view carousel up to date.
* This also handles changes in state and applies them to the media carousel like the expansion.
*/
@Singleton
class MediaViewManager @Inject constructor(
private val context: Context,
@Main private val foregroundExecutor: Executor,
@Background private val backgroundExecutor: DelayableExecutor,
private val localBluetoothManager: LocalBluetoothManager?,
private val visualStabilityManager: VisualStabilityManager,
private val activityStarter: ActivityStarter,
mediaManager: MediaDataManager
) {
private var playerWidth: Int = 0
private var playerWidthPlusPadding: Int = 0
private var desiredState: MediaHost.MediaHostState? = null
private var currentState: MediaState? = null
val mediaCarousel: HorizontalScrollView
private val mediaContent: ViewGroup
private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private val visualStabilityCallback = ::reorderAllPlayers
private var activeMediaIndex: Int = 0
private var scrollIntoCurrentMedia: Int = 0
private var currentlyExpanded = true
set(value) {
if (field != value) {
field = value
for (player in mediaPlayers.values) {
player.setListening(field)
}
}
}
private val scrollChangedListener = object : View.OnScrollChangeListener {
override fun onScrollChange(v: View?, scrollX: Int, scrollY: Int, oldScrollX: Int,
oldScrollY: Int) {
if (playerWidthPlusPadding == 0) {
return
}
onMediaScrollingChanged(scrollX / playerWidthPlusPadding,
scrollX % playerWidthPlusPadding)
}
}
init {
mediaCarousel = inflateMediaCarousel()
mediaCarousel.setOnScrollChangeListener(scrollChangedListener)
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
mediaManager.addListener(object : MediaDataManager.Listener {
override fun onMediaDataLoaded(key: String, data: MediaData) {
updateView(key, data)
updatePlayerVisibilities()
}
override fun onMediaDataRemoved(key: String) {
val removed = mediaPlayers.remove(key)
removed?.apply {
val beforeActive = mediaContent.indexOfChild(removed.view) <= activeMediaIndex
mediaContent.removeView(removed.view)
removed.onDestroy()
updateMediaPaddings()
if (beforeActive) {
// also update the index here since the scroll below might not always lead
// to a scrolling changed
activeMediaIndex = Math.max(0, activeMediaIndex - 1)
mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX
- playerWidthPlusPadding, 0)
}
updatePlayerVisibilities()
}
}
})
}
private fun inflateMediaCarousel(): HorizontalScrollView {
return LayoutInflater.from(context).inflate(R.layout.media_carousel,
UniqueObjectHostView(context), false) as HorizontalScrollView
}
private fun reorderAllPlayers() {
for (mediaPlayer in mediaPlayers.values) {
val view = mediaPlayer.view
if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) {
mediaContent.removeView(view)
mediaContent.addView(view, 0)
}
}
updateMediaPaddings()
updatePlayerVisibilities()
}
private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
val wasScrolledIn = scrollIntoCurrentMedia != 0
scrollIntoCurrentMedia = scrollInAmount
val nowScrolledIn = scrollIntoCurrentMedia != 0
if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
activeMediaIndex = newIndex
updatePlayerVisibilities()
}
}
private fun updatePlayerVisibilities() {
val scrolledIn = scrollIntoCurrentMedia != 0
for (i in 0 until mediaContent.childCount) {
val view = mediaContent.getChildAt(i)
val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn)
view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
}
}
private fun updateView(key: String, data: MediaData) {
var existingPlayer = mediaPlayers[key]
if (existingPlayer == null) {
// Set up listener for device changes
// TODO: integrate with MediaTransferManager?
val imm = InfoMediaManager(context, data.packageName,
null /* notification */, localBluetoothManager)
val routeManager = LocalMediaManager(context, localBluetoothManager,
imm, data.packageName)
existingPlayer = MediaControlPanel(context, mediaContent, routeManager,
foregroundExecutor, backgroundExecutor, activityStarter)
mediaPlayers[key] = existingPlayer
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
existingPlayer.view.setLayoutParams(lp)
existingPlayer.setListening(currentlyExpanded)
if (existingPlayer.isPlaying) {
mediaContent.addView(existingPlayer.view, 0)
} else {
mediaContent.addView(existingPlayer.view)
}
updatePlayerToCurrentState(existingPlayer)
} else if (existingPlayer.isPlaying &&
mediaContent.indexOfChild(existingPlayer.view) != 0) {
if (visualStabilityManager.isReorderingAllowed) {
mediaContent.removeView(existingPlayer.view)
mediaContent.addView(existingPlayer.view, 0)
} else {
visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback)
}
}
existingPlayer.bind(data)
// Resetting the progress to make sure it's taken into account for the latest
// motion model
existingPlayer.view.progress = currentState?.expansion ?: 0.0f
updateMediaPaddings()
}
private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) {
if (desiredState != null && desiredState!!.measurementInput != null) {
// make sure the player width is set to the current state
existingPlayer.setPlayerWidth(playerWidth)
}
}
private fun updateMediaPaddings() {
val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
val childCount = mediaContent.childCount
for (i in 0 until childCount) {
val mediaView = mediaContent.getChildAt(i)
val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
if (layoutParams.marginEnd != desiredPaddingEnd) {
layoutParams.marginEnd = desiredPaddingEnd
mediaView.layoutParams = layoutParams
}
}
}
/**
* Set the current state of a view. This is updated often during animations and we shouldn't
* do anything expensive.
*/
fun setCurrentState(state: MediaState) {
currentState = state
currentlyExpanded = state.expansion > 0
for (mediaPlayer in mediaPlayers.values) {
val view = mediaPlayer.view
view.progress = state.expansion
}
}
/**
* The desired location of this view has changed. We should remeasure the view to match
* the new bounds and kick off bounds animations if necessary.
* If an animation is happening, an animation is kicked of externally, which sets a new
* current state until we reach the targetState.
*
* @param desiredState the target state we're transitioning to
* @param animate should this be animated
*/
fun onDesiredLocationChanged(desiredState: MediaState?, animate: Boolean, duration: Long,
startDelay: Long) {
if (desiredState is MediaHost.MediaHostState) {
// This is a hosting view, let's remeasure our players
this.desiredState = desiredState
val width = desiredState.boundsOnScreen.width()
if (playerWidth != width) {
setPlayerWidth(width)
for (mediaPlayer in mediaPlayers.values) {
if (animate && mediaPlayer.view.visibility == View.VISIBLE) {
mediaPlayer.animatePendingSizeChange(duration, startDelay)
}
}
val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0
val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0
var left = 0
for (i in 0 until mediaContent.childCount) {
val view = mediaContent.getChildAt(i)
view.measure(widthSpec, heightSpec)
view.layout(left, 0, left + width, view.measuredHeight)
left = left + playerWidthPlusPadding
}
}
}
}
fun setPlayerWidth(width: Int) {
if (width != playerWidth) {
playerWidth = width
playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize(
R.dimen.qs_media_padding)
for (mediaPlayer in mediaPlayers.values) {
mediaPlayer.setPlayerWidth(width)
}
// The player width has changed, let's update the scroll position to make sure
// it's still at the same place
var newScroll = activeMediaIndex * playerWidthPlusPadding
if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
newScroll += playerWidthPlusPadding
- (scrollIntoCurrentMedia - playerWidthPlusPadding)
} else {
newScroll += scrollIntoCurrentMedia
}
mediaCarousel.scrollX = newScroll
}
}
/**
* Get a measurement for the given input state. This measures the first player and returns
* its bounds as if it were measured with the given measurement dimensions
*/
fun obtainMeasurement(input: MediaMeasurementInput) : MeasurementOutput? {
val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null
// Let's measure the size of the first player and return its height
val previousProgress = firstPlayer.view.progress
val previousRight = firstPlayer.view.right
val previousBottom = firstPlayer.view.bottom
firstPlayer.view.progress = input.expansion
firstPlayer.measure(input)
// Relayouting is necessary in motionlayout to obtain its size properly ....
firstPlayer.view.layout(0, 0, firstPlayer.view.measuredWidth,
firstPlayer.view.measuredHeight)
val result = MeasurementOutput(firstPlayer.view.measuredWidth,
firstPlayer.view.measuredHeight)
firstPlayer.view.progress = previousProgress
if (desiredState != null) {
// remeasure it to the old size again!
firstPlayer.measure(desiredState!!.measurementInput)
firstPlayer.view.layout(0, 0, previousRight, previousBottom)
}
return result
}
fun onViewReattached() {
if (desiredState is MediaHost.MediaHostState) {
// HACK: MotionLayout doesn't always properly reevalate the state, let's kick of
// a measure to force it.
val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0
val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0
for (mediaPlayer in mediaPlayers.values) {
mediaPlayer.view.measure(widthSpec, heightSpec)
}
}
}
}