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

Commit 5381e194 authored by Beth Thibodeau's avatar Beth Thibodeau Committed by Automerger Merge Worker
Browse files

Merge "Update when PlaybackState state or actions change" into tm-dev am: 80017b1f

parents 06c4607d 80017b1f
Loading
Loading
Loading
Loading
+28 −10
Original line number 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.
        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(
+75 −9
Original line number Diff line number Diff line
@@ -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
    }
}
+11 −0
Original line number 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(
        TAG,
        LogLevel.DEBUG,
+34 −0
Original line number 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.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
     */
+167 −0
Original line number Diff line number Diff line
@@ -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))
    }
}