diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index a3839ae538..39bca5a8f7 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -25,28 +25,113 @@ import android.os.Bundle import androidx.media3.common.AudioAttributes import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.CommandButton import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.ControllerInfo +import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture class PlaybackService : MediaLibraryService() { + private val librarySessionCallback = CustomMediaLibrarySessionCallback() + private lateinit var player: ExoPlayer private lateinit var mediaLibrarySession: MediaLibrarySession - private val librarySessionCallback = CustomMediaLibrarySessionCallback() + private lateinit var customCommands: List + + private var customLayout = ImmutableList.of() companion object { private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch" private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" + private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = + "android.media3.session.demo.SHUFFLE_ON" + private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = + "android.media3.session.demo.SHUFFLE_OFF" + } + + override fun onCreate() { + super.onCreate() + customCommands = + listOf( + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY) + ), + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY) + ) + ) + customLayout = ImmutableList.of(customCommands[0]) + initializeSessionAndPlayer() + } + + override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession { + return mediaLibrarySession + } + + override fun onDestroy() { + player.release() + mediaLibrarySession.release() + super.onDestroy() } private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback { + + override fun onConnect( + session: MediaSession, + controller: ControllerInfo + ): MediaSession.ConnectionResult { + val connectionResult = super.onConnect(session, controller) + val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() + customCommands.forEach { commandButton -> + // Add custom command to available session commands. + commandButton.sessionCommand?.let { availableSessionCommands.add(it) } + } + return MediaSession.ConnectionResult.accept( + availableSessionCommands.build(), + connectionResult.availablePlayerCommands + ) + } + + override fun onPostConnect(session: MediaSession, controller: ControllerInfo) { + if (!customLayout.isEmpty() && controller.controllerVersion != 0) { + // Let Media3 controller (for instance the MediaNotificationProvider) know about the custom + // layout right after it connected. + ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout)) + } + } + + override fun onCustomCommand( + session: MediaSession, + controller: ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) { + // Enable shuffling. + player.shuffleModeEnabled = true + // Change the custom layout to contain the `Disable shuffling` command. + customLayout = ImmutableList.of(customCommands[1]) + // Send the updated custom layout to controllers. + session.setCustomLayout(customLayout) + } else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) { + // Disable shuffling. + player.shuffleModeEnabled = false + // Change the custom layout to contain the `Enable shuffling` command. + customLayout = ImmutableList.of(customCommands[0]) + // Send the updated custom layout to controllers. + session.setCustomLayout(customLayout) + } + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + override fun onGetLibraryRoot( session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, + browser: ControllerInfo, params: LibraryParams? ): ListenableFuture> { return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params)) @@ -54,7 +139,7 @@ class PlaybackService : MediaLibraryService() { override fun onGetItem( session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, + browser: ControllerInfo, mediaId: String ): ListenableFuture> { val item = @@ -65,9 +150,24 @@ class PlaybackService : MediaLibraryService() { return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) } + override fun onSubscribe( + session: MediaLibrarySession, + browser: ControllerInfo, + parentId: String, + params: LibraryParams? + ): ListenableFuture> { + val children = + MediaItemTree.getChildren(parentId) + ?: return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + session.notifyChildrenChanged(browser, parentId, children.size, params) + return Futures.immediateFuture(LibraryResult.ofVoid()) + } + override fun onGetChildren( session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, + browser: ControllerInfo, parentId: String, page: Int, pageSize: Int, @@ -82,6 +182,26 @@ class PlaybackService : MediaLibraryService() { return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) } + override fun onSetMediaUri( + session: MediaSession, + controller: ControllerInfo, + uri: Uri, + extras: Bundle + ): Int { + + if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || + uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) + ) { + val searchQuery = + uri.getQueryParameter("query") ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED + setMediaItemFromSearchQuery(searchQuery) + + return SessionResult.RESULT_SUCCESS + } else { + return SessionResult.RESULT_ERROR_NOT_SUPPORTED + } + } + private fun setMediaItemFromSearchQuery(query: String) { // Only accept query with pattern "play [Title]" or "[Title]" // Where [Title]: must be exactly matched @@ -96,66 +216,6 @@ class PlaybackService : MediaLibraryService() { val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() player.setMediaItem(item) } - - override fun onSetMediaUri( - session: MediaSession, - controller: MediaSession.ControllerInfo, - uri: Uri, - extras: Bundle - ): Int { - - if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || - uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) - ) { - var searchQuery = - uri.getQueryParameter("query") ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED - setMediaItemFromSearchQuery(searchQuery) - - return SessionResult.RESULT_SUCCESS - } else { - return SessionResult.RESULT_ERROR_NOT_SUPPORTED - } - } - - override fun onSubscribe( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - parentId: String, - params: LibraryParams? - ): ListenableFuture> { - val children = - MediaItemTree.getChildren(parentId) - ?: return Futures.immediateFuture( - LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) - ) - session.notifyChildrenChanged(browser, parentId, children.size, params) - return Futures.immediateFuture(LibraryResult.ofVoid()) - } - } - - private class CustomMediaItemFiller : MediaSession.MediaItemFiller { - override fun fillInLocalConfiguration( - session: MediaSession, - controller: MediaSession.ControllerInfo, - mediaItem: MediaItem - ): MediaItem { - return MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem - } - } - - override fun onCreate() { - super.onCreate() - initializeSessionAndPlayer() - } - - override fun onDestroy() { - player.release() - mediaLibrarySession.release() - super.onDestroy() - } - - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession { - return mediaLibrarySession } private fun initializeSessionAndPlayer() { @@ -165,13 +225,10 @@ class PlaybackService : MediaLibraryService() { .build() MediaItemTree.initialize(assets) - val parentScreenIntent = Intent(this, MainActivity::class.java) - val intent = Intent(this, PlayerActivity::class.java) - - val pendingIntent = + val sessionActivityPendingIntent = TaskStackBuilder.create(this).run { - addNextIntent(parentScreenIntent) - addNextIntent(intent) + addNextIntent(Intent(this@PlaybackService, MainActivity::class.java)) + addNextIntent(Intent(this@PlaybackService, PlayerActivity::class.java)) val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0 getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT) @@ -180,7 +237,39 @@ class PlaybackService : MediaLibraryService() { mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) .setMediaItemFiller(CustomMediaItemFiller()) - .setSessionActivity(pendingIntent) + .setSessionActivity(sessionActivityPendingIntent) .build() + if (!customLayout.isEmpty()) { + // Send custom layout to legacy session. + mediaLibrarySession.setCustomLayout(customLayout) + } + } + + private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton { + val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON + return CommandButton.Builder() + .setDisplayName( + getString( + if (isOn) R.string.exo_controls_shuffle_on_description + else R.string.exo_controls_shuffle_off_description + ) + ) + .setSessionCommand(sessionCommand) + .setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on) + .build() + } + + private fun ignoreFuture(customLayout: ListenableFuture) { + /* Do nothing. */ + } + + private class CustomMediaItemFiller : MediaSession.MediaItemFiller { + override fun fillInLocalConfiguration( + session: MediaSession, + controller: ControllerInfo, + mediaItem: MediaItem + ): MediaItem { + return MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem + } } }