Loading packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt +7 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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 Loading @@ -105,7 +111,7 @@ class Media3ActionFactoryTest : SysuiTestCase() { kosmos.mediaLogger, kosmos.looper, handler, kosmos.testScope, testScope, kosmos.execution, ) Loading packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt +31 −9 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading @@ -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 -> Loading @@ -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) } } Loading Loading @@ -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, Loading @@ -139,6 +146,7 @@ constructor( ) val nextButton = getStandardAction( packageName, m3controller, token, Player.COMMAND_SEEK_TO_NEXT, Loading Loading @@ -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, Loading @@ -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), ) Loading @@ -238,7 +247,7 @@ constructor( else -> { MediaAction( icon = getIconForAction(command), action = { executeAction(token, command) }, action = { executeAction(packageName, token, command) }, contentDescription = getDescriptionForAction(command), background = null, ) Loading @@ -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, ) Loading Loading @@ -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) { Loading Loading @@ -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()) } } packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt +24 −0 Original line number Diff line number Diff line Loading @@ -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" } Loading packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.kt +17 −5 Original line number Diff line number Diff line Loading @@ -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. * Loading @@ -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 } } } packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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) } Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt +7 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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 Loading @@ -105,7 +111,7 @@ class Media3ActionFactoryTest : SysuiTestCase() { kosmos.mediaLogger, kosmos.looper, handler, kosmos.testScope, testScope, kosmos.execution, ) Loading
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt +31 −9 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading @@ -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 -> Loading @@ -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) } } Loading Loading @@ -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, Loading @@ -139,6 +146,7 @@ constructor( ) val nextButton = getStandardAction( packageName, m3controller, token, Player.COMMAND_SEEK_TO_NEXT, Loading Loading @@ -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, Loading @@ -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), ) Loading @@ -238,7 +247,7 @@ constructor( else -> { MediaAction( icon = getIconForAction(command), action = { executeAction(token, command) }, action = { executeAction(packageName, token, command) }, contentDescription = getDescriptionForAction(command), background = null, ) Loading @@ -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, ) Loading Loading @@ -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) { Loading Loading @@ -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()) } }
packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt +24 −0 Original line number Diff line number Diff line Loading @@ -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" } Loading
packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.kt +17 −5 Original line number Diff line number Diff line Loading @@ -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. * Loading @@ -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 } } }
packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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) } Loading