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.
|
// Set up links back into the pipeline for listeners that need to send events upstream.
|
||||||
mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
|
mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
|
||||||
setTimedOut(key, timedOut) }
|
setTimedOut(key, timedOut) }
|
||||||
|
mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
|
||||||
|
updateState(key, state) }
|
||||||
mediaResumeListener.setManager(this)
|
mediaResumeListener.setManager(this)
|
||||||
mediaDataFilter.mediaDataManager = 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) {
|
private fun removeEntry(key: String) {
|
||||||
mediaEntries.remove(key)?.let {
|
mediaEntries.remove(key)?.let {
|
||||||
logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
|
logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
|
||||||
@@ -673,11 +690,8 @@ class MediaDataManager(
|
|||||||
// Otherwise, use the notification actions
|
// Otherwise, use the notification actions
|
||||||
var actionIcons: List<MediaAction> = emptyList()
|
var actionIcons: List<MediaAction> = emptyList()
|
||||||
var actionsToShowCollapsed: List<Int> = emptyList()
|
var actionsToShowCollapsed: List<Int> = emptyList()
|
||||||
var semanticActions: MediaButton? = null
|
val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
|
||||||
if (mediaFlags.areMediaSessionActionsEnabled(sbn.packageName, sbn.user) &&
|
if (semanticActions == null) {
|
||||||
mediaController.playbackState != null) {
|
|
||||||
semanticActions = createActionsFromState(sbn.packageName, mediaController)
|
|
||||||
} else {
|
|
||||||
val actions = createActionsFromNotification(sbn)
|
val actions = createActionsFromNotification(sbn)
|
||||||
actionIcons = actions.first
|
actionIcons = actions.first
|
||||||
actionsToShowCollapsed = actions.second
|
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
|
* @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
|
* of those actions should be shown in the compact player
|
||||||
*/
|
*/
|
||||||
private fun createActionsFromState(packageName: String, controller: MediaController):
|
private fun createActionsFromState(
|
||||||
MediaButton? {
|
packageName: String,
|
||||||
|
controller: MediaController,
|
||||||
|
user: UserHandle
|
||||||
|
): MediaButton? {
|
||||||
val state = controller.playbackState
|
val state = controller.playbackState
|
||||||
if (state == null) {
|
if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
|
||||||
return MediaButton()
|
return null
|
||||||
}
|
}
|
||||||
// First, check for} standard actions
|
|
||||||
|
// First, check for standard actions
|
||||||
val playOrPause = if (isConnectingState(state.state)) {
|
val playOrPause = if (isConnectingState(state.state)) {
|
||||||
// Spinner needs to be animating to render anything. Start it here.
|
// Spinner needs to be animating to render anything. Start it here.
|
||||||
val drawable = context.getDrawable(
|
val drawable = context.getDrawable(
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
lateinit var timeoutCallback: (String, Boolean) -> Unit
|
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(
|
override fun onMediaDataLoaded(
|
||||||
key: String,
|
key: String,
|
||||||
oldKey: String?,
|
oldKey: String?,
|
||||||
@@ -85,17 +92,17 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
reusedListener?.let {
|
reusedListener?.let {
|
||||||
val wasPlaying = it.playing ?: false
|
val wasPlaying = it.isPlaying()
|
||||||
logger.logUpdateListener(key, wasPlaying)
|
logger.logUpdateListener(key, wasPlaying)
|
||||||
it.mediaData = data
|
it.mediaData = data
|
||||||
it.key = key
|
it.key = key
|
||||||
mediaListeners[key] = it
|
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
|
// 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
|
// its state. Doing it now would lead to reentrant callbacks, so let's wait
|
||||||
// until we're done.
|
// until we're done.
|
||||||
mainExecutor.execute {
|
mainExecutor.execute {
|
||||||
if (mediaListeners[key]?.playing == true) {
|
if (mediaListeners[key]?.isPlaying() == true) {
|
||||||
logger.logDelayedUpdate(key)
|
logger.logDelayedUpdate(key)
|
||||||
timeoutCallback.invoke(key, false /* timedOut */)
|
timeoutCallback.invoke(key, false /* timedOut */)
|
||||||
}
|
}
|
||||||
@@ -121,7 +128,7 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
) : MediaController.Callback() {
|
) : MediaController.Callback() {
|
||||||
|
|
||||||
var timedOut = false
|
var timedOut = false
|
||||||
var playing: Boolean? = null
|
var lastState: PlaybackState? = null
|
||||||
var resumption: Boolean? = null
|
var resumption: Boolean? = null
|
||||||
var destroyed = false
|
var destroyed = false
|
||||||
|
|
||||||
@@ -145,6 +152,9 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
private var mediaController: MediaController? = null
|
private var mediaController: MediaController? = null
|
||||||
private var cancellation: Runnable? = null
|
private var cancellation: Runnable? = null
|
||||||
|
|
||||||
|
fun Int.isPlaying() = isPlayingState(this)
|
||||||
|
fun isPlaying() = lastState?.state?.isPlaying() ?: false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mediaData = data
|
mediaData = data
|
||||||
}
|
}
|
||||||
@@ -175,16 +185,26 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
|
private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
|
||||||
logger.logPlaybackState(key, state)
|
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
|
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
|
return
|
||||||
}
|
}
|
||||||
playing = isPlaying
|
|
||||||
resumption = mediaData.resumption
|
resumption = mediaData.resumption
|
||||||
|
|
||||||
if (!isPlaying) {
|
val playing = isPlaying()
|
||||||
logger.logScheduleTimeout(key, isPlaying, resumption!!)
|
if (!playing) {
|
||||||
|
logger.logScheduleTimeout(key, playing, resumption!!)
|
||||||
if (cancellation != null && !resumptionChanged) {
|
if (cancellation != null && !resumptionChanged) {
|
||||||
// if the media changed resume state, we'll need to adjust the timeout length
|
// if the media changed resume state, we'll need to adjust the timeout length
|
||||||
logger.logCancelIgnored(key)
|
logger.logCancelIgnored(key)
|
||||||
@@ -220,4 +240,50 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
cancellation = null
|
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(
|
fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = buffer.log(
|
||||||
TAG,
|
TAG,
|
||||||
LogLevel.DEBUG,
|
LogLevel.DEBUG,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.android.systemui.statusbar.SbnBuilder
|
|||||||
import com.android.systemui.tuner.TunerService
|
import com.android.systemui.tuner.TunerService
|
||||||
import com.android.systemui.util.concurrency.FakeExecutor
|
import com.android.systemui.util.concurrency.FakeExecutor
|
||||||
import com.android.systemui.util.mockito.any
|
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.capture
|
||||||
import com.android.systemui.util.mockito.eq
|
import com.android.systemui.util.mockito.eq
|
||||||
import com.android.systemui.util.time.FakeSystemClock
|
import com.android.systemui.util.time.FakeSystemClock
|
||||||
@@ -47,6 +48,7 @@ import org.mockito.Mock
|
|||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.mockito.Mockito.never
|
import org.mockito.Mockito.never
|
||||||
import org.mockito.Mockito.reset
|
import org.mockito.Mockito.reset
|
||||||
|
import org.mockito.Mockito.times
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||||
import org.mockito.junit.MockitoJUnit
|
import org.mockito.junit.MockitoJUnit
|
||||||
@@ -938,6 +940,38 @@ class MediaDataManagerTest : SysuiTestCase() {
|
|||||||
eq(instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE))
|
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
|
* 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
|
@Mock private lateinit var logger: MediaTimeoutLogger
|
||||||
private lateinit var executor: FakeExecutor
|
private lateinit var executor: FakeExecutor
|
||||||
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
|
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
|
||||||
|
@Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
|
||||||
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
|
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
|
||||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||||
private lateinit var metadataBuilder: MediaMetadata.Builder
|
private lateinit var metadataBuilder: MediaMetadata.Builder
|
||||||
@@ -80,6 +81,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
|||||||
executor = FakeExecutor(FakeSystemClock())
|
executor = FakeExecutor(FakeSystemClock())
|
||||||
mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor, logger)
|
mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor, logger)
|
||||||
mediaTimeoutListener.timeoutCallback = timeoutCallback
|
mediaTimeoutListener.timeoutCallback = timeoutCallback
|
||||||
|
mediaTimeoutListener.stateCallback = stateCallback
|
||||||
|
|
||||||
// Create a media session and notification for testing.
|
// Create a media session and notification for testing.
|
||||||
metadataBuilder = MediaMetadata.Builder().apply {
|
metadataBuilder = MediaMetadata.Builder().apply {
|
||||||
@@ -368,4 +370,169 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
|||||||
// THEN the timeout runnable is cancelled
|
// THEN the timeout runnable is cancelled
|
||||||
assertThat(executor.numPending()).isEqualTo(0)
|
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