Merge "Update when PlaybackState state or actions change" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
80017b1f2f
@@ -261,6 +261,8 @@ class MediaDataManager(
|
||||
// Set up links back into the pipeline for listeners that need to send events upstream.
|
||||
mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
|
||||
setTimedOut(key, timedOut) }
|
||||
mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
|
||||
updateState(key, state) }
|
||||
mediaResumeListener.setManager(this)
|
||||
mediaDataFilter.mediaDataManager = this
|
||||
|
||||
@@ -502,6 +504,21 @@ class MediaDataManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the player's [PlaybackState] has been updated with new actions and/or state
|
||||
*/
|
||||
private fun updateState(key: String, state: PlaybackState) {
|
||||
mediaEntries.get(key)?.let {
|
||||
val actions = createActionsFromState(it.packageName,
|
||||
mediaControllerFactory.create(it.token), UserHandle(it.userId))
|
||||
val data = it.copy(
|
||||
semanticActions = actions,
|
||||
isPlaying = isPlayingState(state.state))
|
||||
if (DEBUG) Log.d(TAG, "State updated outside of notification")
|
||||
onMediaDataLoaded(key, key, data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeEntry(key: String) {
|
||||
mediaEntries.remove(key)?.let {
|
||||
logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
|
||||
@@ -673,11 +690,8 @@ class MediaDataManager(
|
||||
// Otherwise, use the notification actions
|
||||
var actionIcons: List<MediaAction> = emptyList()
|
||||
var actionsToShowCollapsed: List<Int> = emptyList()
|
||||
var semanticActions: MediaButton? = null
|
||||
if (mediaFlags.areMediaSessionActionsEnabled(sbn.packageName, sbn.user) &&
|
||||
mediaController.playbackState != null) {
|
||||
semanticActions = createActionsFromState(sbn.packageName, mediaController)
|
||||
} else {
|
||||
val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
|
||||
if (semanticActions == null) {
|
||||
val actions = createActionsFromNotification(sbn)
|
||||
actionIcons = actions.first
|
||||
actionsToShowCollapsed = actions.second
|
||||
@@ -789,13 +803,17 @@ class MediaDataManager(
|
||||
* @return a Pair consisting of a list of media actions, and a list of ints representing which
|
||||
* of those actions should be shown in the compact player
|
||||
*/
|
||||
private fun createActionsFromState(packageName: String, controller: MediaController):
|
||||
MediaButton? {
|
||||
private fun createActionsFromState(
|
||||
packageName: String,
|
||||
controller: MediaController,
|
||||
user: UserHandle
|
||||
): MediaButton? {
|
||||
val state = controller.playbackState
|
||||
if (state == null) {
|
||||
return MediaButton()
|
||||
if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
|
||||
return null
|
||||
}
|
||||
// First, check for} standard actions
|
||||
|
||||
// First, check for standard actions
|
||||
val playOrPause = if (isConnectingState(state.state)) {
|
||||
// Spinner needs to be animating to render anything. Start it here.
|
||||
val drawable = context.getDrawable(
|
||||
|
||||
@@ -55,6 +55,13 @@ class MediaTimeoutListener @Inject constructor(
|
||||
*/
|
||||
lateinit var timeoutCallback: (String, Boolean) -> Unit
|
||||
|
||||
/**
|
||||
* Callback representing that a media object [PlaybackState] has changed.
|
||||
* @param key Media control unique identifier
|
||||
* @param state The new [PlaybackState]
|
||||
*/
|
||||
lateinit var stateCallback: (String, PlaybackState) -> Unit
|
||||
|
||||
override fun onMediaDataLoaded(
|
||||
key: String,
|
||||
oldKey: String?,
|
||||
@@ -85,17 +92,17 @@ class MediaTimeoutListener @Inject constructor(
|
||||
}
|
||||
|
||||
reusedListener?.let {
|
||||
val wasPlaying = it.playing ?: false
|
||||
val wasPlaying = it.isPlaying()
|
||||
logger.logUpdateListener(key, wasPlaying)
|
||||
it.mediaData = data
|
||||
it.key = key
|
||||
mediaListeners[key] = it
|
||||
if (wasPlaying != it.playing) {
|
||||
if (wasPlaying != it.isPlaying()) {
|
||||
// If a player becomes active because of a migration, we'll need to broadcast
|
||||
// its state. Doing it now would lead to reentrant callbacks, so let's wait
|
||||
// until we're done.
|
||||
mainExecutor.execute {
|
||||
if (mediaListeners[key]?.playing == true) {
|
||||
if (mediaListeners[key]?.isPlaying() == true) {
|
||||
logger.logDelayedUpdate(key)
|
||||
timeoutCallback.invoke(key, false /* timedOut */)
|
||||
}
|
||||
@@ -121,7 +128,7 @@ class MediaTimeoutListener @Inject constructor(
|
||||
) : MediaController.Callback() {
|
||||
|
||||
var timedOut = false
|
||||
var playing: Boolean? = null
|
||||
var lastState: PlaybackState? = null
|
||||
var resumption: Boolean? = null
|
||||
var destroyed = false
|
||||
|
||||
@@ -145,6 +152,9 @@ class MediaTimeoutListener @Inject constructor(
|
||||
private var mediaController: MediaController? = null
|
||||
private var cancellation: Runnable? = null
|
||||
|
||||
fun Int.isPlaying() = isPlayingState(this)
|
||||
fun isPlaying() = lastState?.state?.isPlaying() ?: false
|
||||
|
||||
init {
|
||||
mediaData = data
|
||||
}
|
||||
@@ -175,16 +185,26 @@ class MediaTimeoutListener @Inject constructor(
|
||||
private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
|
||||
logger.logPlaybackState(key, state)
|
||||
|
||||
val isPlaying = state != null && isPlayingState(state.state)
|
||||
val playingStateSame = (state?.state?.isPlaying() == isPlaying())
|
||||
val actionsSame = (lastState?.actions == state?.actions) &&
|
||||
areCustomActionListsEqual(lastState?.customActions, state?.customActions)
|
||||
val resumptionChanged = resumption != mediaData.resumption
|
||||
if (playing == isPlaying && playing != null && !resumptionChanged) {
|
||||
|
||||
lastState = state
|
||||
|
||||
if ((!actionsSame || !playingStateSame) && state != null && dispatchEvents) {
|
||||
logger.logStateCallback(key)
|
||||
stateCallback.invoke(key, state)
|
||||
}
|
||||
|
||||
if (playingStateSame && !resumptionChanged) {
|
||||
return
|
||||
}
|
||||
playing = isPlaying
|
||||
resumption = mediaData.resumption
|
||||
|
||||
if (!isPlaying) {
|
||||
logger.logScheduleTimeout(key, isPlaying, resumption!!)
|
||||
val playing = isPlaying()
|
||||
if (!playing) {
|
||||
logger.logScheduleTimeout(key, playing, resumption!!)
|
||||
if (cancellation != null && !resumptionChanged) {
|
||||
// if the media changed resume state, we'll need to adjust the timeout length
|
||||
logger.logCancelIgnored(key)
|
||||
@@ -220,4 +240,50 @@ class MediaTimeoutListener @Inject constructor(
|
||||
cancellation = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun areCustomActionListsEqual(
|
||||
first: List<PlaybackState.CustomAction>?,
|
||||
second: List<PlaybackState.CustomAction>?
|
||||
): Boolean {
|
||||
// Same object, or both null
|
||||
if (first === second) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Only one null, or different number of actions
|
||||
if ((first == null || second == null) || (first.size != second.size)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare individual actions
|
||||
first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) ->
|
||||
if (!areCustomActionsEqual(firstAction, secondAction)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun areCustomActionsEqual(
|
||||
firstAction: PlaybackState.CustomAction,
|
||||
secondAction: PlaybackState.CustomAction
|
||||
): Boolean {
|
||||
if (firstAction.action != secondAction.action ||
|
||||
firstAction.name != secondAction.name ||
|
||||
firstAction.icon != secondAction.icon) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ((firstAction.extras == null) != (secondAction.extras == null)) {
|
||||
return false
|
||||
}
|
||||
if (firstAction.extras != null) {
|
||||
firstAction.extras.keySet().forEach { key ->
|
||||
if (firstAction.extras.get(key) != secondAction.extras.get(key)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,17 @@ class MediaTimeoutLogger @Inject constructor(
|
||||
}
|
||||
)
|
||||
|
||||
fun logStateCallback(key: String) = buffer.log(
|
||||
TAG,
|
||||
LogLevel.VERBOSE,
|
||||
{
|
||||
str1 = key
|
||||
},
|
||||
{
|
||||
"dispatching state update for $key"
|
||||
}
|
||||
)
|
||||
|
||||
fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = buffer.log(
|
||||
TAG,
|
||||
LogLevel.DEBUG,
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.android.systemui.statusbar.SbnBuilder
|
||||
import com.android.systemui.tuner.TunerService
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.argumentCaptor
|
||||
import com.android.systemui.util.mockito.capture
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
@@ -47,6 +48,7 @@ import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
@@ -938,6 +940,38 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
eq(instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPlaybackStateChange_keyExists_callsListener() {
|
||||
// Notification has been added
|
||||
addNotificationAndLoad()
|
||||
val callbackCaptor = argumentCaptor<(String, PlaybackState) -> Unit>()
|
||||
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
|
||||
|
||||
// Callback gets an updated state
|
||||
val state = PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PLAYING, 0L, 1f)
|
||||
.build()
|
||||
callbackCaptor.value.invoke(KEY, state)
|
||||
|
||||
// Listener is notified of updated state
|
||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY),
|
||||
capture(mediaDataCaptor), eq(true), eq(0), eq(false))
|
||||
assertThat(mediaDataCaptor.value.isPlaying).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPlaybackStateChange_keyDoesNotExist_doesNothing() {
|
||||
val state = PlaybackState.Builder().build()
|
||||
val callbackCaptor = argumentCaptor<(String, PlaybackState) -> Unit>()
|
||||
verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
|
||||
|
||||
// No media added with this key
|
||||
|
||||
callbackCaptor.value.invoke(KEY, state)
|
||||
verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(),
|
||||
anyBoolean())
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to add a media notification and capture the resulting MediaData
|
||||
*/
|
||||
|
||||
@@ -65,6 +65,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
@Mock private lateinit var logger: MediaTimeoutLogger
|
||||
private lateinit var executor: FakeExecutor
|
||||
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
|
||||
@Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
|
||||
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
|
||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||
private lateinit var metadataBuilder: MediaMetadata.Builder
|
||||
@@ -80,6 +81,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
executor = FakeExecutor(FakeSystemClock())
|
||||
mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor, logger)
|
||||
mediaTimeoutListener.timeoutCallback = timeoutCallback
|
||||
mediaTimeoutListener.stateCallback = stateCallback
|
||||
|
||||
// Create a media session and notification for testing.
|
||||
metadataBuilder = MediaMetadata.Builder().apply {
|
||||
@@ -368,4 +370,169 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
// THEN the timeout runnable is cancelled
|
||||
assertThat(executor.numPending()).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() {
|
||||
// Load media data once
|
||||
val pausedState = PlaybackState.Builder()
|
||||
.setActions(PlaybackState.ACTION_PAUSE)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(pausedState)
|
||||
|
||||
// When media data is loaded again, with different actions
|
||||
val playingState = PlaybackState.Builder()
|
||||
.setActions(PlaybackState.ACTION_PLAY)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(playingState)
|
||||
|
||||
// Then the callback is not invoked
|
||||
verify(stateCallback, never()).invoke(eq(KEY), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() {
|
||||
// Load media data once
|
||||
val pausedState = PlaybackState.Builder()
|
||||
.setActions(PlaybackState.ACTION_PAUSE)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(pausedState)
|
||||
|
||||
// When the playback state changes, and has different actions
|
||||
val playingState = PlaybackState.Builder()
|
||||
.setActions(PlaybackState.ACTION_PLAY)
|
||||
.build()
|
||||
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
|
||||
|
||||
// Then the callback is invoked
|
||||
verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() {
|
||||
val customOne = PlaybackState.CustomAction.Builder(
|
||||
"ACTION_1",
|
||||
"custom action 1",
|
||||
android.R.drawable.ic_media_ff)
|
||||
.build()
|
||||
val pausedState = PlaybackState.Builder()
|
||||
.setActions(PlaybackState.ACTION_PAUSE)
|
||||
.addCustomAction(customOne)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(pausedState)
|
||||
|
||||
// When the playback state actions change
|
||||
val customTwo = PlaybackState.CustomAction.Builder(
|
||||
"ACTION_2",
|
||||
"custom action 2",
|
||||
android.R.drawable.ic_media_rew)
|
||||
.build()
|
||||
val pausedStateTwoActions = PlaybackState.Builder()
|
||||
.setActions(PlaybackState.ACTION_PAUSE)
|
||||
.addCustomAction(customOne)
|
||||
.addCustomAction(customTwo)
|
||||
.build()
|
||||
mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
|
||||
|
||||
// Then the callback is invoked
|
||||
verify(stateCallback).invoke(eq(KEY), eq(pausedStateTwoActions!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPlaybackStateChanged_sameActions_noCallback() {
|
||||
val stateWithActions = PlaybackState.Builder()
|
||||
.setActions(PlaybackState.ACTION_PLAY)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(stateWithActions)
|
||||
|
||||
// When the playback state updates with the same actions
|
||||
mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions)
|
||||
|
||||
// Then the callback is not invoked again
|
||||
verify(stateCallback, never()).invoke(eq(KEY), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPlaybackStateChanged_sameCustomActions_noCallback() {
|
||||
val actionName = "custom action"
|
||||
val actionIcon = android.R.drawable.ic_media_ff
|
||||
val customOne = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon)
|
||||
.build()
|
||||
val stateOne = PlaybackState.Builder()
|
||||
.setActions(PlaybackState.ACTION_PAUSE)
|
||||
.addCustomAction(customOne)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(stateOne)
|
||||
|
||||
// When the playback state is updated, but has the same actions
|
||||
val customTwo = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon)
|
||||
.build()
|
||||
val stateTwo = PlaybackState.Builder()
|
||||
.setActions(PlaybackState.ACTION_PAUSE)
|
||||
.addCustomAction(customTwo)
|
||||
.build()
|
||||
mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo)
|
||||
|
||||
// Then the callback is not invoked
|
||||
verify(stateCallback, never()).invoke(eq(KEY), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnMediaDataLoaded_isPlayingChanged_noCallback() {
|
||||
// Load media data in paused state
|
||||
val pausedState = PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(pausedState)
|
||||
|
||||
// When media data is loaded again but playing
|
||||
val playingState = PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PLAYING, 0L, 1f)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(playingState)
|
||||
|
||||
// Then the callback is not invoked
|
||||
verify(stateCallback, never()).invoke(eq(KEY), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() {
|
||||
// Load media data in paused state
|
||||
val pausedState = PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(pausedState)
|
||||
|
||||
// When the playback state changes to playing
|
||||
val playingState = PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PLAYING, 0L, 1f)
|
||||
.build()
|
||||
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
|
||||
|
||||
// Then the callback is invoked
|
||||
verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPlaybackStateChanged_isPlayingSame_noCallback() {
|
||||
// Load media data in paused state
|
||||
val pausedState = PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
|
||||
.build()
|
||||
loadMediaDataWithPlaybackState(pausedState)
|
||||
|
||||
// When the playback state is updated, but still not playing
|
||||
val playingState = PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_STOPPED, 0L, 0f)
|
||||
.build()
|
||||
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
|
||||
|
||||
// Then the callback is not invoked
|
||||
verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
|
||||
}
|
||||
|
||||
private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
|
||||
`when`(mediaController.playbackState).thenReturn(state)
|
||||
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
|
||||
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user