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

Commit 2f5095a9 authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Catch SecurityException with media3 controller

This exception can be thrown if apps reject the connection, so catch and
log if it occurs

Bug: 385155201
Test: atest Media3ActionFactoryTest
Flag: com.android.systemui.media_controls_button_media3
Change-Id: I0090e86dd3d94f205294a930b5ca7e50283b2ac3
parent a7656c9a
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ 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.SessionResult
import androidx.media3.session.SessionToken
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -41,6 +42,8 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.execution
import com.google.common.collect.ImmutableList
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -60,6 +63,7 @@ private const val PACKAGE_NAME = "package_name"
private const val CUSTOM_ACTION_NAME = "Custom Action"
private const val CUSTOM_ACTION_COMMAND = "custom-action"

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWithLooper
@RunWith(AndroidJUnit4::class)
@@ -84,12 +88,14 @@ class Media3ActionFactoryTest : SysuiTestCase() {
                }
        }
    private val customLayout = ImmutableList.of<CommandButton>()
    private val customCommandFuture = mock<ListenableFuture<SessionResult>>()
    private val media3Controller =
        mock<Media3Controller> {
            on { customLayout } doReturn customLayout
            on { sessionExtras } doReturn Bundle()
            on { isCommandAvailable(any()) } doReturn true
            on { isSessionCommandAvailable(any<SessionCommand>()) } doReturn true
            on { sendCustomCommand(any(), any()) } doReturn customCommandFuture
        }

    private lateinit var underTest: Media3ActionFactory
@@ -105,7 +111,7 @@ class Media3ActionFactoryTest : SysuiTestCase() {
                kosmos.mediaLogger,
                kosmos.looper,
                handler,
                kosmos.testScope,
                testScope,
                kosmos.execution,
            )

+31 −9
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.SessionTokenFactory
import com.android.systemui.res.R
import com.android.systemui.util.concurrency.Execution
import java.util.concurrent.ExecutionException
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -71,7 +72,7 @@ constructor(
     *
     * @param packageName Package name for the media app
     * @param controller The framework [MediaController] for the session
     * @return The media action buttons, or null if the session token is null
     * @return The media action buttons, or null if cannot be created for this session
     */
    suspend fun createActionsFromSession(
        packageName: String,
@@ -80,6 +81,10 @@ constructor(
        // Get the Media3 controller using the legacy token
        val token = tokenFactory.createTokenFromLegacy(sessionToken)
        val m3controller = controllerFactory.create(token, looper)
        if (m3controller == null) {
            logger.logCreateFailed(packageName, "createActionsFromSession")
            return null
        }

        // Build button info
        val buttons = suspendCancellableCoroutine { continuation ->
@@ -89,13 +94,14 @@ constructor(
                    val result = getMedia3Actions(packageName, m3controller, token)
                    continuation.resumeWith(Result.success(result))
                } finally {
                    m3controller.release()
                    m3controller.tryRelease(packageName, logger)
                }
            }
            handler.post(runnable)
            continuation.invokeOnCancellation {
                // Ensure controller is released, even if loading was cancelled partway through
                handler.post(m3controller::release)
                val releaseRunnable = Runnable { m3controller.tryRelease(packageName, logger) }
                handler.post(releaseRunnable)
                handler.removeCallbacks(runnable)
            }
        }
@@ -127,11 +133,12 @@ constructor(
                    com.android.internal.R.drawable.progress_small_material,
                )
            } else {
                getStandardAction(m3controller, token, Player.COMMAND_PLAY_PAUSE)
                getStandardAction(packageName, m3controller, token, Player.COMMAND_PLAY_PAUSE)
            }

        val prevButton =
            getStandardAction(
                packageName,
                m3controller,
                token,
                Player.COMMAND_SEEK_TO_PREVIOUS,
@@ -139,6 +146,7 @@ constructor(
            )
        val nextButton =
            getStandardAction(
                packageName,
                m3controller,
                token,
                Player.COMMAND_SEEK_TO_NEXT,
@@ -208,6 +216,7 @@ constructor(
     * @return A [MediaAction] representing the first supported command, or null if not supported
     */
    private fun getStandardAction(
        packageName: String,
        controller: Media3Controller,
        token: SessionToken,
        vararg commands: @Player.Command Int,
@@ -222,14 +231,14 @@ constructor(
                    if (!controller.isPlaying) {
                        MediaAction(
                            context.getDrawable(R.drawable.ic_media_play),
                            { executeAction(token, Player.COMMAND_PLAY_PAUSE) },
                            { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) },
                            context.getString(R.string.controls_media_button_play),
                            context.getDrawable(R.drawable.ic_media_play_container),
                        )
                    } else {
                        MediaAction(
                            context.getDrawable(R.drawable.ic_media_pause),
                            { executeAction(token, Player.COMMAND_PLAY_PAUSE) },
                            { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) },
                            context.getString(R.string.controls_media_button_pause),
                            context.getDrawable(R.drawable.ic_media_pause_container),
                        )
@@ -238,7 +247,7 @@ constructor(
                else -> {
                    MediaAction(
                        icon = getIconForAction(command),
                        action = { executeAction(token, command) },
                        action = { executeAction(packageName, token, command) },
                        contentDescription = getDescriptionForAction(command),
                        background = null,
                    )
@@ -256,7 +265,7 @@ constructor(
    ): MediaAction {
        return MediaAction(
            getIconForAction(customAction, packageName),
            { executeAction(token, Player.COMMAND_INVALID, customAction) },
            { executeAction(packageName, token, Player.COMMAND_INVALID, customAction) },
            customAction.displayName,
            null,
        )
@@ -308,12 +317,17 @@ constructor(
    }

    private fun executeAction(
        packageName: String,
        token: SessionToken,
        command: Int,
        customAction: CommandButton? = null,
    ) {
        bgScope.launch {
            val controller = controllerFactory.create(token, looper)
            if (controller == null) {
                logger.logCreateFailed(packageName, "executeAction")
                return@launch
            }
            handler.post {
                try {
                    when (command) {
@@ -347,9 +361,17 @@ constructor(
                        else -> logger.logMedia3UnsupportedCommand(command.toString())
                    }
                } finally {
                    controller.release()
                    controller.tryRelease(packageName, logger)
                }
            }
        }
    }
}

private fun Media3Controller.tryRelease(packageName: String, logger: MediaLogger) {
    try {
        this.release()
    } catch (e: ExecutionException) {
        logger.logReleaseFailed(packageName, e.cause.toString())
    }
}
+24 −0
Original line number Diff line number Diff line
@@ -144,6 +144,30 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) {
        buffer.log(TAG, LogLevel.DEBUG, { str1 = command }, { "Unsupported media3 command $str1" })
    }

    fun logCreateFailed(pkg: String, method: String) {
        buffer.log(
            TAG,
            LogLevel.DEBUG,
            {
                str1 = pkg
                str2 = method
            },
            { "Controller create failed for $str1 ($str2)" },
        )
    }

    fun logReleaseFailed(pkg: String, cause: String) {
        buffer.log(
            TAG,
            LogLevel.DEBUG,
            {
                str1 = pkg
                str2 = cause
            },
            { "Controller release failed for $str1 ($str2)" },
        )
    }

    companion object {
        private const val TAG = "MediaLog"
    }
+17 −5
Original line number Diff line number Diff line
@@ -19,13 +19,17 @@ import android.content.Context
import android.media.session.MediaController
import android.media.session.MediaSession
import android.os.Looper
import android.util.Log
import androidx.concurrent.futures.await
import androidx.media3.session.MediaController as Media3Controller
import androidx.media3.session.SessionToken
import java.util.concurrent.ExecutionException
import javax.inject.Inject

/** Testable wrapper for media controller construction */
open class MediaControllerFactory @Inject constructor(private val context: Context) {
    private val TAG = "MediaControllerFactory"

    /**
     * Creates a new [MediaController] from the framework session token.
     *
@@ -41,10 +45,18 @@ open class MediaControllerFactory @Inject constructor(private val context: Conte
     * @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 {
    open suspend fun create(token: SessionToken, looper: Looper): Media3Controller? {
        try {
            return Media3Controller.Builder(context, token)
                .setApplicationLooper(looper)
                .buildAsync()
                .await()
        } catch (e: ExecutionException) {
            if (e.cause is SecurityException) {
                // The session rejected the connection
                Log.d(TAG, "SecurityException creating media3 controller")
            }
            return null
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -35,7 +35,7 @@ class FakeMediaControllerFactory(context: Context) : MediaControllerFactory(cont
        return mediaControllersForToken[token]!!
    }

    override suspend fun create(token: SessionToken, looper: Looper): Media3Controller {
    override suspend fun create(token: SessionToken, looper: Looper): Media3Controller? {
        return media3Controller ?: super.create(token, looper)
    }