Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 80017b1f authored by Beth Thibodeau's avatar Beth Thibodeau Committed by Android (Google) Code Review
Browse files

Merge "Update when PlaybackState state or actions change" into tm-dev

parents e6c8903e ecf1d11f
Loading
Loading
Loading
Loading
+28 −10
Original line number Original line Diff line number Diff line
@@ -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(
+75 −9
Original line number Original line Diff line number Diff line
@@ -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
    }
}
}
+11 −0
Original line number Original line Diff line number Diff line
@@ -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,
+34 −0
Original line number Original line Diff line number Diff line
@@ -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
     */
     */
+167 −0
Original line number Original line Diff line number Diff line
@@ -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))
    }
}
}