Merge "Hook up media player resumption to UI" into rvc-dev am: df9b697804 am: c3a46cd1b0 am: 489a4e4282
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/11801909 Change-Id: I62bfe8694f281dbe01384e125074adefd9372fd7
This commit is contained in:
@@ -96,13 +96,13 @@ public class MediaControlPanel {
|
||||
*/
|
||||
@Inject
|
||||
public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
|
||||
ActivityStarter activityStarter, MediaHostStatesManager mediaHostStatesManager,
|
||||
ActivityStarter activityStarter, MediaViewController mediaViewController,
|
||||
SeekBarViewModel seekBarViewModel) {
|
||||
mContext = context;
|
||||
mBackgroundExecutor = backgroundExecutor;
|
||||
mActivityStarter = activityStarter;
|
||||
mSeekBarViewModel = seekBarViewModel;
|
||||
mMediaViewController = new MediaViewController(context, mediaHostStatesManager);
|
||||
mMediaViewController = mediaViewController;
|
||||
loadDimens();
|
||||
}
|
||||
|
||||
@@ -365,14 +365,6 @@ public class MediaControlPanel {
|
||||
return artwork;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the token for the current media session
|
||||
* @return the token
|
||||
*/
|
||||
public MediaSession.Token getMediaSessionToken() {
|
||||
return mToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current media controller
|
||||
* @return the controller
|
||||
@@ -381,25 +373,6 @@ public class MediaControlPanel {
|
||||
return mController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the package associated with the current media controller
|
||||
* @return the package name, or null if no controller
|
||||
*/
|
||||
public String getMediaPlayerPackage() {
|
||||
if (mController == null) {
|
||||
return null;
|
||||
}
|
||||
return mController.getPackageName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this player has an attached media session.
|
||||
* @return whether there is a controller with a current media session.
|
||||
*/
|
||||
public boolean hasMediaSession() {
|
||||
return mController != null && mController.getPlaybackState() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the media controlled by this player is currently playing
|
||||
* @return whether it is playing, or false if no controller information
|
||||
|
||||
@@ -25,19 +25,65 @@ import android.media.session.MediaSession
|
||||
data class MediaData(
|
||||
val initialized: Boolean = false,
|
||||
val backgroundColor: Int,
|
||||
/**
|
||||
* App name that will be displayed on the player.
|
||||
*/
|
||||
val app: String?,
|
||||
/**
|
||||
* Icon shown on player, close to app name.
|
||||
*/
|
||||
val appIcon: Drawable?,
|
||||
/**
|
||||
* Artist name.
|
||||
*/
|
||||
val artist: CharSequence?,
|
||||
/**
|
||||
* Song name.
|
||||
*/
|
||||
val song: CharSequence?,
|
||||
/**
|
||||
* Album artwork.
|
||||
*/
|
||||
val artwork: Icon?,
|
||||
/**
|
||||
* List of actions that can be performed on the player: prev, next, play, pause, etc.
|
||||
*/
|
||||
val actions: List<MediaAction>,
|
||||
/**
|
||||
* Same as above, but shown on smaller versions of the player, like in QQS or keyguard.
|
||||
*/
|
||||
val actionsToShowInCompact: List<Int>,
|
||||
/**
|
||||
* Package name of the app that's posting the media.
|
||||
*/
|
||||
val packageName: String,
|
||||
/**
|
||||
* Unique media session identifier.
|
||||
*/
|
||||
val token: MediaSession.Token?,
|
||||
/**
|
||||
* Action to perform when the player is tapped.
|
||||
* This is unrelated to {@link #actions}.
|
||||
*/
|
||||
val clickIntent: PendingIntent?,
|
||||
/**
|
||||
* Where the media is playing: phone, headphones, ear buds, remote session.
|
||||
*/
|
||||
val device: MediaDeviceData?,
|
||||
/**
|
||||
* When active, a player will be displayed on keyguard and quick-quick settings.
|
||||
* This is unrelated to the stream being playing or not, a player will not be active if
|
||||
* timed out, or in resumption mode.
|
||||
*/
|
||||
var active: Boolean,
|
||||
/**
|
||||
* Action that should be performed to restart a non active session.
|
||||
*/
|
||||
var resumeAction: Runnable?,
|
||||
val notificationKey: String = "INVALID",
|
||||
/**
|
||||
* Notification key for cancelling a media player after a timeout (when not using resumption.)
|
||||
*/
|
||||
val notificationKey: String? = null,
|
||||
var hasCheckedForResume: Boolean = false
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ private const val LUMINOSITY_THRESHOLD = 0.05f
|
||||
private const val SATURATION_MULTIPLIER = 0.8f
|
||||
|
||||
private val LOADING = MediaData(false, 0, null, null, null, null, null,
|
||||
emptyList(), emptyList(), "INVALID", null, null, null, null)
|
||||
emptyList(), emptyList(), "INVALID", null, null, null, true, null)
|
||||
|
||||
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
|
||||
if (!sbn.notification.hasMediaSession()) {
|
||||
@@ -88,12 +88,12 @@ fun isMediaNotification(sbn: StatusBarNotification): Boolean {
|
||||
class MediaDataManager @Inject constructor(
|
||||
private val context: Context,
|
||||
private val mediaControllerFactory: MediaControllerFactory,
|
||||
private val mediaTimeoutListener: MediaTimeoutListener,
|
||||
private val notificationEntryManager: NotificationEntryManager,
|
||||
private val mediaResumeListener: MediaResumeListener,
|
||||
@Background private val backgroundExecutor: Executor,
|
||||
@Main private val foregroundExecutor: Executor,
|
||||
private val broadcastDispatcher: BroadcastDispatcher
|
||||
broadcastDispatcher: BroadcastDispatcher,
|
||||
mediaTimeoutListener: MediaTimeoutListener,
|
||||
mediaResumeListener: MediaResumeListener
|
||||
) {
|
||||
|
||||
private val listeners: MutableSet<Listener> = mutableSetOf()
|
||||
@@ -131,7 +131,6 @@ class MediaDataManager @Inject constructor(
|
||||
mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
|
||||
setTimedOut(token, timedOut) }
|
||||
addListener(mediaTimeoutListener)
|
||||
|
||||
if (useMediaResumption) {
|
||||
mediaResumeListener.addTrackToResumeCallback = { desc: MediaDescription,
|
||||
resumeAction: Runnable, token: MediaSession.Token, appName: String,
|
||||
@@ -215,7 +214,7 @@ class MediaDataManager @Inject constructor(
|
||||
mediaEntries.put(packageName, resumeData)
|
||||
}
|
||||
backgroundExecutor.execute {
|
||||
loadMediaDataInBg(desc, action, token, appName, appIntent, packageName)
|
||||
loadMediaDataInBgForResumption(desc, action, token, appName, appIntent, packageName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,16 +254,21 @@ class MediaDataManager @Inject constructor(
|
||||
fun removeListener(listener: Listener) = listeners.remove(listener)
|
||||
|
||||
private fun setTimedOut(token: String, timedOut: Boolean) {
|
||||
if (!timedOut) {
|
||||
return
|
||||
}
|
||||
mediaEntries[token]?.let {
|
||||
notificationEntryManager.removeNotification(it.notificationKey, null /* ranking */,
|
||||
UNDEFINED_DISMISS_REASON)
|
||||
if (Utils.useMediaResumption(context)) {
|
||||
if (it.active == !timedOut) {
|
||||
return
|
||||
}
|
||||
it.active = !timedOut
|
||||
onMediaDataLoaded(token, token, it)
|
||||
} else {
|
||||
notificationEntryManager.removeNotification(it.notificationKey, null /* ranking */,
|
||||
UNDEFINED_DISMISS_REASON)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMediaDataInBg(
|
||||
private fun loadMediaDataInBgForResumption(
|
||||
desc: MediaDescription,
|
||||
resumeAction: Runnable,
|
||||
token: MediaSession.Token,
|
||||
@@ -272,11 +276,6 @@ class MediaDataManager @Inject constructor(
|
||||
appIntent: PendingIntent,
|
||||
packageName: String
|
||||
) {
|
||||
if (resumeAction == null) {
|
||||
Log.e(TAG, "Resume action cannot be null")
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(desc.title)) {
|
||||
Log.e(TAG, "Description incomplete")
|
||||
return
|
||||
@@ -298,8 +297,9 @@ class MediaDataManager @Inject constructor(
|
||||
val mediaAction = getResumeMediaAction(resumeAction)
|
||||
foregroundExecutor.execute {
|
||||
onMediaDataLoaded(packageName, null, MediaData(true, Color.DKGRAY, appName,
|
||||
null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
|
||||
packageName, token, appIntent, null, resumeAction, packageName))
|
||||
null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
|
||||
packageName, token, appIntent, device = null, active = false,
|
||||
resumeAction = resumeAction))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +430,8 @@ class MediaDataManager @Inject constructor(
|
||||
foregroundExecutor.execute {
|
||||
onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
|
||||
song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
|
||||
notif.contentIntent, null, resumeAction, key))
|
||||
notif.contentIntent, null, active = true, resumeAction = resumeAction,
|
||||
notificationKey = key))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,13 +529,13 @@ class MediaDataManager @Inject constructor(
|
||||
/**
|
||||
* Are there any media notifications active?
|
||||
*/
|
||||
fun hasActiveMedia() = mediaEntries.any({ isActive(it.value) })
|
||||
fun hasActiveMedia() = mediaEntries.any { it.value.active }
|
||||
|
||||
fun isActive(data: MediaData): Boolean {
|
||||
if (data.token == null) {
|
||||
fun isActive(token: MediaSession.Token?): Boolean {
|
||||
if (token == null) {
|
||||
return false
|
||||
}
|
||||
val controller = mediaControllerFactory.create(data.token)
|
||||
val controller = mediaControllerFactory.create(token)
|
||||
val state = controller?.playbackState?.state
|
||||
return state != null && NotificationMediaManager.isActiveState(state)
|
||||
}
|
||||
@@ -542,7 +543,7 @@ class MediaDataManager @Inject constructor(
|
||||
/**
|
||||
* Are there any media entries, including resume controls?
|
||||
*/
|
||||
fun hasAnyMedia() = mediaEntries.isNotEmpty()
|
||||
fun hasAnyMedia() = if (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia()
|
||||
|
||||
interface Listener {
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.View.OnAttachStateChangeListener
|
||||
@@ -20,8 +21,6 @@ class MediaHost @Inject constructor(
|
||||
var location: Int = -1
|
||||
private set
|
||||
var visibleChangedListener: ((Boolean) -> Unit)? = null
|
||||
var visible: Boolean = false
|
||||
private set
|
||||
|
||||
private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
|
||||
|
||||
@@ -109,16 +108,17 @@ class MediaHost @Inject constructor(
|
||||
}
|
||||
|
||||
private fun updateViewVisibility() {
|
||||
if (showsOnlyActiveMedia) {
|
||||
visible = mediaDataManager.hasActiveMedia()
|
||||
visible = if (showsOnlyActiveMedia) {
|
||||
mediaDataManager.hasActiveMedia()
|
||||
} else {
|
||||
visible = mediaDataManager.hasAnyMedia()
|
||||
mediaDataManager.hasAnyMedia()
|
||||
}
|
||||
hostView.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
visibleChangedListener?.invoke(visible)
|
||||
}
|
||||
|
||||
class MediaHostStateHolder @Inject constructor() : MediaHostState {
|
||||
private var gonePivot: PointF = PointF()
|
||||
|
||||
override var measurementInput: MeasurementInput? = null
|
||||
set(value) {
|
||||
@@ -144,6 +144,25 @@ class MediaHost @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override var visible: Boolean = true
|
||||
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]
|
||||
*/
|
||||
@@ -157,6 +176,8 @@ class MediaHost @Inject constructor(
|
||||
mediaHostState.expansion = expansion
|
||||
mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
|
||||
mediaHostState.measurementInput = measurementInput?.copy()
|
||||
mediaHostState.visible = visible
|
||||
mediaHostState.gonePivot.set(gonePivot)
|
||||
return mediaHostState
|
||||
}
|
||||
|
||||
@@ -173,6 +194,12 @@ class MediaHost @Inject constructor(
|
||||
if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
|
||||
return false
|
||||
}
|
||||
if (visible != other.visible) {
|
||||
return false
|
||||
}
|
||||
if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -180,6 +207,8 @@ class MediaHost @Inject constructor(
|
||||
var result = measurementInput?.hashCode() ?: 0
|
||||
result = 31 * result + expansion.hashCode()
|
||||
result = 31 * result + showsOnlyActiveMedia.hashCode()
|
||||
result = 31 * result + if (visible) 1 else 2
|
||||
result = 31 * result + gonePivot.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -194,7 +223,8 @@ interface MediaHostState {
|
||||
var measurementInput: MeasurementInput?
|
||||
|
||||
/**
|
||||
* The expansion of the player, 0 for fully collapsed, 1 for fully expanded
|
||||
* The expansion of the player, 0 for fully collapsed (up to 3 actions), 1 for fully expanded
|
||||
* (up to 5 actions.)
|
||||
*/
|
||||
var expansion: Float
|
||||
|
||||
@@ -203,6 +233,30 @@ interface MediaHostState {
|
||||
*/
|
||||
var showsOnlyActiveMedia: Boolean
|
||||
|
||||
/**
|
||||
* If the view should be VISIBLE or GONE.
|
||||
*/
|
||||
var visible: 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
|
||||
*/
|
||||
|
||||
@@ -43,6 +43,11 @@ class MediaTimeoutListener @Inject constructor(
|
||||
|
||||
private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* Callback representing that a media object is now expired:
|
||||
* @param token Media session unique identifier
|
||||
* @param pauseTimeuot True when expired for {@code PAUSED_MEDIA_TIMEOUT}
|
||||
*/
|
||||
lateinit var timeoutCallback: (String, Boolean) -> Unit
|
||||
|
||||
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
|
||||
@@ -112,11 +117,11 @@ class MediaTimeoutListener @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun expireMediaTimeout(mediaNotificationKey: String, reason: String) {
|
||||
private fun expireMediaTimeout(mediaKey: String, reason: String) {
|
||||
cancellation?.apply {
|
||||
if (DEBUG) {
|
||||
Log.v(TAG,
|
||||
"media timeout cancelled for $mediaNotificationKey, reason: $reason")
|
||||
"media timeout cancelled for $mediaKey, reason: $reason")
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
@@ -17,20 +17,22 @@
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.util.animation.MeasurementOutput
|
||||
import com.android.systemui.util.animation.TransitionLayout
|
||||
import com.android.systemui.util.animation.TransitionLayoutController
|
||||
import com.android.systemui.util.animation.TransitionViewState
|
||||
import com.android.systemui.util.animation.MeasurementOutput
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* A class responsible for controlling a single instance of a media player handling interactions
|
||||
* with the view instance and keeping the media view states up to date.
|
||||
*/
|
||||
class MediaViewController(
|
||||
class MediaViewController @Inject constructor(
|
||||
context: Context,
|
||||
val mediaHostStatesManager: MediaHostStatesManager
|
||||
private val mediaHostStatesManager: MediaHostStatesManager
|
||||
) {
|
||||
|
||||
private var firstRefresh: Boolean = true
|
||||
@@ -44,7 +46,7 @@ class MediaViewController(
|
||||
/**
|
||||
* A map containing all viewStates for all locations of this mediaState
|
||||
*/
|
||||
private val mViewStates: MutableMap<MediaHostState, TransitionViewState?> = mutableMapOf()
|
||||
private val viewStates: MutableMap<MediaHostState, TransitionViewState?> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* The ending location of the view where it ends when all animations and transitions have
|
||||
@@ -68,6 +70,11 @@ class MediaViewController(
|
||||
*/
|
||||
private val tmpState = TransitionViewState()
|
||||
|
||||
/**
|
||||
* Temporary variable to avoid unnecessary allocations.
|
||||
*/
|
||||
private val tmpPoint = PointF()
|
||||
|
||||
/**
|
||||
* A callback for media state changes
|
||||
*/
|
||||
@@ -125,7 +132,7 @@ class MediaViewController(
|
||||
* it's not available, it will recreate one by measuring, which may be expensive.
|
||||
*/
|
||||
private fun obtainViewState(state: MediaHostState): TransitionViewState? {
|
||||
val viewState = mViewStates[state]
|
||||
val viewState = viewStates[state]
|
||||
if (viewState != null) {
|
||||
// we already have cached this measurement, let's continue
|
||||
return viewState
|
||||
@@ -143,7 +150,7 @@ class MediaViewController(
|
||||
// We don't want to cache interpolated or null states as this could quickly fill up
|
||||
// our cache. We only cache the start and the end states since the interpolation
|
||||
// is cheap
|
||||
mViewStates[state.copy()] = result
|
||||
viewStates[state.copy()] = result
|
||||
} else {
|
||||
// This is an interpolated state
|
||||
val startState = state.copy().also { it.expansion = 0.0f }
|
||||
@@ -153,11 +160,13 @@ class MediaViewController(
|
||||
val startViewState = obtainViewState(startState) as TransitionViewState
|
||||
val endState = state.copy().also { it.expansion = 1.0f }
|
||||
val endViewState = obtainViewState(endState) as TransitionViewState
|
||||
tmpPoint.set(startState.getPivotX(), startState.getPivotY())
|
||||
result = TransitionViewState()
|
||||
layoutController.getInterpolatedState(
|
||||
startViewState,
|
||||
endViewState,
|
||||
state.expansion,
|
||||
tmpPoint,
|
||||
result)
|
||||
}
|
||||
} else {
|
||||
@@ -213,11 +222,35 @@ class MediaViewController(
|
||||
|
||||
val shouldAnimate = animateNextStateChange && !applyImmediately
|
||||
|
||||
var startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
|
||||
var endHostState = mediaHostStatesManager.mediaHostStates[endLocation]
|
||||
var swappedStartState = false
|
||||
var swappedEndState = false
|
||||
|
||||
// if we're going from or to a non visible state, let's grab the visible one and animate
|
||||
// the view being clipped instead.
|
||||
if (endHostState?.visible != true) {
|
||||
endHostState = startHostState
|
||||
swappedEndState = true
|
||||
}
|
||||
if (startHostState?.visible != true) {
|
||||
startHostState = endHostState
|
||||
swappedStartState = true
|
||||
}
|
||||
if (startHostState == null || endHostState == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var endViewState = obtainViewState(endHostState) ?: return
|
||||
if (swappedEndState) {
|
||||
endViewState = endViewState.copy()
|
||||
endViewState.height = 0
|
||||
}
|
||||
|
||||
// Obtain the view state that we'd want to be at the end
|
||||
// The view might not be bound yet or has never been measured and in that case will be
|
||||
// reset once the state is fully available
|
||||
val endState = obtainViewStateForLocation(endLocation) ?: return
|
||||
layoutController.setMeasureState(endState)
|
||||
layoutController.setMeasureState(endViewState)
|
||||
|
||||
// If the view isn't bound, we can drop the animation, otherwise we'll executute it
|
||||
animateNextStateChange = false
|
||||
@@ -225,24 +258,43 @@ class MediaViewController(
|
||||
return
|
||||
}
|
||||
|
||||
val startState = obtainViewStateForLocation(startLocation)
|
||||
var startViewState = obtainViewState(startHostState)
|
||||
if (swappedStartState) {
|
||||
startViewState = startViewState?.copy()
|
||||
startViewState?.height = 0
|
||||
}
|
||||
|
||||
val result: TransitionViewState?
|
||||
if (transitionProgress == 1.0f || startState == null) {
|
||||
result = endState
|
||||
result = if (transitionProgress == 1.0f || startViewState == null) {
|
||||
endViewState
|
||||
} else if (transitionProgress == 0.0f) {
|
||||
result = startState
|
||||
startViewState
|
||||
} else {
|
||||
layoutController.getInterpolatedState(startState, endState, transitionProgress,
|
||||
tmpState)
|
||||
result = tmpState
|
||||
if (swappedEndState || swappedStartState) {
|
||||
tmpPoint.set(startHostState.getPivotX(), startHostState.getPivotY())
|
||||
} else {
|
||||
tmpPoint.set(0.0f, 0.0f)
|
||||
}
|
||||
layoutController.getInterpolatedState(startViewState, endViewState, transitionProgress,
|
||||
tmpPoint, tmpState)
|
||||
tmpState
|
||||
}
|
||||
layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
|
||||
animationDelay)
|
||||
}
|
||||
|
||||
private fun obtainViewStateForLocation(location: Int): TransitionViewState? {
|
||||
val mediaState = mediaHostStatesManager.mediaHostStates[location] ?: return null
|
||||
return obtainViewState(mediaState)
|
||||
/**
|
||||
* Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation].
|
||||
* In the event of [location] not being visible, [locationWhenHidden] will be used instead.
|
||||
*
|
||||
* @param location Target
|
||||
* @param locationWhenHidden Location that will be used when the target is not
|
||||
* [MediaHost.visible]
|
||||
* @return State require for executing a transition, and also the respective [MediaHost].
|
||||
*/
|
||||
private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? {
|
||||
val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null
|
||||
return obtainViewState(mediaHostState)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,8 +302,7 @@ class MediaViewController(
|
||||
* This updates the width the view will me measured with.
|
||||
*/
|
||||
fun onLocationPreChange(@MediaLocation newLocation: Int) {
|
||||
val viewState = obtainViewStateForLocation(newLocation)
|
||||
viewState?.let {
|
||||
obtainViewStateForLocation(newLocation)?.let {
|
||||
layoutController.setMeasureState(it)
|
||||
}
|
||||
}
|
||||
@@ -271,7 +322,7 @@ class MediaViewController(
|
||||
fun refreshState() {
|
||||
if (!firstRefresh) {
|
||||
// Let's clear all of our measurements and recreate them!
|
||||
mViewStates.clear()
|
||||
viewStates.clear()
|
||||
setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress,
|
||||
applyImmediately = false)
|
||||
}
|
||||
|
||||
@@ -235,6 +235,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
||||
protected void initMediaHostState() {
|
||||
mMediaHost.setExpansion(1.0f);
|
||||
mMediaHost.setShowsOnlyActiveMedia(false);
|
||||
// Reveal player with some parallax (1.0f would also work)
|
||||
mMediaHost.setGonePivot(0.0f, 0.8f);
|
||||
mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.systemui.util.animation
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
@@ -151,6 +152,11 @@ class TransitionLayout @JvmOverloads constructor(
|
||||
val layoutTop = top
|
||||
setLeftTopRightBottom(layoutLeft, layoutTop, layoutLeft + currentState.width,
|
||||
layoutTop + currentState.height)
|
||||
val bounds = clipBounds ?: Rect()
|
||||
bounds.set(left, top, right, bottom)
|
||||
clipBounds = bounds
|
||||
translationX = currentState.translation.x
|
||||
translationY = currentState.translation.y
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,11 +240,13 @@ class TransitionViewState {
|
||||
var widgetStates: MutableMap<Int, WidgetState> = mutableMapOf()
|
||||
var width: Int = 0
|
||||
var height: Int = 0
|
||||
val translation = PointF()
|
||||
fun copy(reusedState: TransitionViewState? = null): TransitionViewState {
|
||||
// we need a deep copy of this, so we can't use a data class
|
||||
val copy = reusedState ?: TransitionViewState()
|
||||
copy.width = width
|
||||
copy.height = height
|
||||
copy.translation.set(translation.x, translation.y)
|
||||
for (entry in widgetStates) {
|
||||
copy.widgetStates[entry.key] = entry.value.copy()
|
||||
}
|
||||
@@ -256,6 +264,7 @@ class TransitionViewState {
|
||||
}
|
||||
width = transitionLayout.measuredWidth
|
||||
height = transitionLayout.measuredHeight
|
||||
translation.set(0.0f, 0.0f)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.systemui.util.animation
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.graphics.PointF
|
||||
import android.util.MathUtils
|
||||
import com.android.systemui.Interpolators
|
||||
|
||||
@@ -43,6 +44,7 @@ open class TransitionLayoutController {
|
||||
private var currentState = TransitionViewState()
|
||||
private var animationStartState: TransitionViewState? = null
|
||||
private var state = TransitionViewState()
|
||||
private var pivot = PointF()
|
||||
private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
|
||||
|
||||
init {
|
||||
@@ -63,6 +65,7 @@ open class TransitionLayoutController {
|
||||
startState = animationStartState!!,
|
||||
endState = state,
|
||||
progress = animator.animatedFraction,
|
||||
pivot = pivot,
|
||||
resultState = currentState)
|
||||
view.setState(currentState)
|
||||
}
|
||||
@@ -75,8 +78,10 @@ open class TransitionLayoutController {
|
||||
startState: TransitionViewState,
|
||||
endState: TransitionViewState,
|
||||
progress: Float,
|
||||
pivot: PointF,
|
||||
resultState: TransitionViewState
|
||||
) {
|
||||
this.pivot.set(pivot)
|
||||
val view = transitionLayout ?: return
|
||||
val childCount = view.childCount
|
||||
for (i in 0 until childCount) {
|
||||
@@ -178,6 +183,8 @@ open class TransitionLayoutController {
|
||||
progress).toInt()
|
||||
height = MathUtils.lerp(startState.height.toFloat(), endState.height.toFloat(),
|
||||
progress).toInt()
|
||||
translation.x = (endState.width - width) * pivot.x
|
||||
translation.y = (endState.height - height) * pivot.y
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
@@ -75,9 +76,9 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var holder: PlayerViewHolder
|
||||
@Mock private lateinit var view: TransitionLayout
|
||||
@Mock private lateinit var mediaHostStatesManager: MediaHostStatesManager
|
||||
@Mock private lateinit var seekBarViewModel: SeekBarViewModel
|
||||
@Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress>
|
||||
@Mock private lateinit var mediaViewController: MediaViewController
|
||||
private lateinit var appIcon: ImageView
|
||||
private lateinit var appName: TextView
|
||||
private lateinit var albumView: ImageView
|
||||
@@ -104,8 +105,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
bgExecutor = FakeExecutor(FakeSystemClock())
|
||||
whenever(mediaViewController.expandedLayout).thenReturn(mock(ConstraintSet::class.java))
|
||||
whenever(mediaViewController.collapsedLayout).thenReturn(mock(ConstraintSet::class.java))
|
||||
|
||||
player = MediaControlPanel(context, bgExecutor, activityStarter, mediaHostStatesManager,
|
||||
player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController,
|
||||
seekBarViewModel)
|
||||
whenever(seekBarViewModel.progress).thenReturn(seekBarData)
|
||||
|
||||
@@ -172,7 +175,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun bindWhenUnattached() {
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, null, null, device, null)
|
||||
emptyList(), PACKAGE, null, null, device, true, null)
|
||||
player.bind(state)
|
||||
assertThat(player.isPlaying()).isFalse()
|
||||
}
|
||||
@@ -181,7 +184,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
fun bindText() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
|
||||
player.bind(state)
|
||||
assertThat(appName.getText()).isEqualTo(APP)
|
||||
assertThat(titleText.getText()).isEqualTo(TITLE)
|
||||
@@ -192,7 +195,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
fun bindBackgroundColor() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
|
||||
player.bind(state)
|
||||
val list = ArgumentCaptor.forClass(ColorStateList::class.java)
|
||||
verify(view).setBackgroundTintList(list.capture())
|
||||
@@ -203,7 +206,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
fun bindDevice() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
|
||||
player.bind(state)
|
||||
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
|
||||
assertThat(seamless.isEnabled()).isTrue()
|
||||
@@ -213,7 +216,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
fun bindDisabledDevice() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, null)
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, true, null)
|
||||
player.bind(state)
|
||||
assertThat(seamless.isEnabled()).isFalse()
|
||||
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
|
||||
@@ -224,7 +227,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
fun bindNullDevice() {
|
||||
player.attach(holder)
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, null, null)
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null)
|
||||
player.bind(state)
|
||||
assertThat(seamless.isEnabled()).isTrue()
|
||||
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
|
||||
|
||||
@@ -79,7 +79,8 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||
mManager.addListener(mListener);
|
||||
|
||||
mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null,
|
||||
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, null, KEY, false);
|
||||
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, KEY,
|
||||
false);
|
||||
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||
setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken()))
|
||||
}
|
||||
mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
|
||||
emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null)
|
||||
emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null,
|
||||
device = null, active = true, resumeAction = null)
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
@@ -93,7 +93,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
}
|
||||
session.setActive(true)
|
||||
mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
|
||||
emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null)
|
||||
emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null,
|
||||
device = null, active = true, resumeAction = null)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user