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

Commit 51f80344 authored by Automerger Merge Worker's avatar Automerger Merge Worker
Browse files

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

Merge "Merge "Update when PlaybackState state or actions change" into tm-dev am: 80017b1f am: 5381e194" into tm-d1-dev-plus-aosp am: 437f036d

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/18014216



Change-Id: I8f5cf00d33d0a87503fbfc5906f5d2ea5f407672
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents c7b648f7 437f036d
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))
    }
}