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

Commit 34ef2450 authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Update media3 button tests and error handling

- Use a fake Media3ActionFactory in kosmos
- Wrap media3 handler calls in a try-finally, to ensure that the
  controller disconnects
- Minor edits from code review

Flag: com.android.systemui.media_controls_button_media3
Bug: 360196209
Test: atest Media3ActionFactoryTest MediaDataLoaderTest
Change-Id: Ic91401af5b030da5bfa4ff188d1b55b69b72be5f
parent 4df17359
Loading
Loading
Loading
Loading
+2 −7
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import android.media.session.MediaSession
import android.os.Bundle
import android.os.Handler
import android.os.looper
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.media.utils.MediaConstants
import androidx.media3.common.Player
@@ -69,10 +68,9 @@ class Media3ActionFactoryTest : SysuiTestCase() {
    private val testScope = kosmos.testScope
    private val controllerFactory = kosmos.fakeMediaControllerFactory
    private val tokenFactory = kosmos.fakeSessionTokenFactory
    private lateinit var testableLooper: TestableLooper

    private var commandCaptor = argumentCaptor<SessionCommand>()
    private var runnableCaptor = argumentCaptor<Runnable>()
    private val commandCaptor = argumentCaptor<SessionCommand>()
    private val runnableCaptor = argumentCaptor<Runnable>()

    private val legacyToken = MediaSession.Token(1, null)
    private val token = mock<SessionToken>()
@@ -97,8 +95,6 @@ class Media3ActionFactoryTest : SysuiTestCase() {

    @Before
    fun setup() {
        testableLooper = TestableLooper.get(this)

        underTest =
            Media3ActionFactory(
                context,
@@ -246,7 +242,6 @@ class Media3ActionFactoryTest : SysuiTestCase() {
            assertThat(actions.custom0!!.contentDescription).isEqualTo("$CUSTOM_ACTION_NAME 2")
            actions.custom0!!.action!!.run()
            runCurrent()
            testableLooper.processAllMessages()
            verify(media3Controller).sendCustomCommand(commandCaptor.capture(), any<Bundle>())
            assertThat(commandCaptor.lastValue.customAction).isEqualTo("$CUSTOM_ACTION_COMMAND 2")
            verify(media3Controller).release()
+2 −2
Original line number Diff line number Diff line
@@ -82,7 +82,7 @@ class MediaDataLoaderTest : SysuiTestCase() {
    private val fakeFeatureFlags = kosmos.fakeFeatureFlagsClassic
    private val mediaFlags = kosmos.mediaFlags
    private val mediaControllerFactory = kosmos.fakeMediaControllerFactory
    private val media3ActionFactory = kosmos.media3ActionFactory
    private lateinit var media3ActionFactory: Media3ActionFactory
    private val session = MediaSession(context, "MediaDataLoaderTestSession")
    private val metadataBuilder =
        MediaMetadata.Builder().apply {
@@ -94,6 +94,7 @@ class MediaDataLoaderTest : SysuiTestCase() {

    @Before
    fun setUp() {
        media3ActionFactory = kosmos.media3ActionFactory
        mediaControllerFactory.setControllerForToken(session.sessionToken, mediaController)
        whenever(mediaController.sessionToken).thenReturn(session.sessionToken)
        whenever(mediaController.metadata).then { metadataBuilder.build() }
@@ -311,7 +312,6 @@ class MediaDataLoaderTest : SysuiTestCase() {
                    }
                    build()
                }

            val result = underTest.loadMediaData(KEY, mediaNotification)

            assertThat(result).isNotNull()
+45 −32
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import androidx.annotation.WorkerThread
import androidx.media.utils.MediaConstants
import androidx.media3.common.Player
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaController as Media3Controller
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionToken
import com.android.systemui.dagger.SysUISingleton
@@ -82,10 +83,19 @@ constructor(
        // Build button info
        val buttons = suspendCancellableCoroutine { continuation ->
            // Media3Controller methods must always be called from a specific looper
            handler.post {
            val runnable = Runnable {
                try {
                    val result = getMedia3Actions(packageName, m3controller, token)
                m3controller.release()
                    continuation.resumeWith(Result.success(result))
                } finally {
                    m3controller.release()
                }
            }
            handler.post(runnable)
            continuation.invokeOnCancellation {
                // Ensure controller is released, even if loading was cancelled partway through
                handler.post(m3controller::release)
                handler.removeCallbacks(runnable)
            }
        }
        return buttons
@@ -95,7 +105,7 @@ constructor(
    @WorkerThread
    private fun getMedia3Actions(
        packageName: String,
        m3controller: androidx.media3.session.MediaController,
        m3controller: Media3Controller,
        token: SessionToken,
    ): MediaButton? {
        Assert.isNotMainThread()
@@ -197,7 +207,7 @@ constructor(
     * @return A [MediaAction] representing the first supported command, or null if not supported
     */
    private fun getStandardAction(
        controller: androidx.media3.session.MediaController,
        controller: Media3Controller,
        token: SessionToken,
        vararg commands: @Player.Command Int,
    ): MediaAction? {
@@ -304,6 +314,7 @@ constructor(
        bgScope.launch {
            val controller = controllerFactory.create(token, looper)
            handler.post {
                try {
                    when (command) {
                        Player.COMMAND_PLAY_PAUSE -> {
                            if (controller.isPlaying) controller.pause() else controller.play()
@@ -316,26 +327,28 @@ constructor(
                        Player.COMMAND_SEEK_TO_NEXT -> controller.seekToNext()
                        Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM -> controller.seekToNextMediaItem()
                        Player.COMMAND_INVALID -> {
                        if (
                            customAction != null &&
                                customAction!!.sessionCommand != null &&
                                controller.isSessionCommandAvailable(
                                    customAction!!.sessionCommand!!
                                )
                        ) {
                            if (customAction?.sessionCommand != null) {
                                val sessionCommand = customAction.sessionCommand!!
                                if (controller.isSessionCommandAvailable(sessionCommand)) {
                                    controller.sendCustomCommand(
                                customAction!!.sessionCommand!!,
                                customAction!!.extras,
                                        sessionCommand,
                                        customAction.extras,
                                    )
                                } else {
                                    logger.logMedia3UnsupportedCommand(
                                        "$sessionCommand, action $customAction"
                                    )
                                }
                            } else {
                                logger.logMedia3UnsupportedCommand("$command, action $customAction")
                            }
                        }

                        else -> logger.logMedia3UnsupportedCommand(command.toString())
                    }
                } finally {
                    controller.release()
                }
            }
        }
    }
}
+6 −1
Original line number Diff line number Diff line
@@ -35,7 +35,12 @@ open class MediaControllerFactory @Inject constructor(private val context: Conte
        return MediaController(context, token)
    }

    /** Creates a new [Media3Controller] from a [SessionToken] */
    /**
     * Creates a new [Media3Controller] from the media3 [SessionToken].
     *
     * @param token The token for the session
     * @param looper The looper that will be used for this controller's operations
     */
    open suspend fun create(token: SessionToken, looper: Looper): Media3Controller {
        return Media3Controller.Builder(context, token)
            .setApplicationLooper(looper)
+42 −1
Original line number Diff line number Diff line
@@ -16,7 +16,48 @@

package com.android.systemui.media.controls.domain.pipeline

import android.content.applicationContext
import android.os.Bundle
import android.os.Handler
import android.os.looper
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.android.systemui.Flags
import com.android.systemui.graphics.imageLoader
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.shared.mediaLogger
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
import com.android.systemui.media.controls.util.fakeSessionTokenFactory
import com.google.common.collect.ImmutableList
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

var Kosmos.media3ActionFactory: Media3ActionFactory by Kosmos.Fixture { mock {} }
/**
 * Set up fake [Media3ActionFactory]. Note that tests using this fake will need to be
 * annotated @RunWithLooper
 */
var Kosmos.media3ActionFactory: Media3ActionFactory by
    Kosmos.Fixture {
        if (Flags.mediaControlsButtonMedia3()) {
            val customLayout = ImmutableList.of<CommandButton>()
            val media3Controller =
                mock<MediaController>().also {
                    whenever(it.customLayout).thenReturn(customLayout)
                    whenever(it.sessionExtras).thenReturn(Bundle())
                }
            fakeMediaControllerFactory.setMedia3Controller(media3Controller)
            fakeSessionTokenFactory.setMedia3SessionToken(mock<SessionToken>())
        }
        Media3ActionFactory(
            context = applicationContext,
            imageLoader = imageLoader,
            controllerFactory = fakeMediaControllerFactory,
            tokenFactory = fakeSessionTokenFactory,
            logger = mediaLogger,
            looper = looper,
            handler = Handler(looper),
            bgScope = testScope,
        )
    }