Demonstrate how to use custom actions in the session demo app.

PiperOrigin-RevId: 451410714
This commit is contained in:
bachinger 2022-05-27 16:19:12 +00:00 committed by Marc Baechinger
parent 6c4f6ecf46
commit 9a70bbca05

View File

@ -25,28 +25,113 @@ import android.os.Bundle
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.ControllerInfo
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
class PlaybackService : MediaLibraryService() { class PlaybackService : MediaLibraryService() {
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var mediaLibrarySession: MediaLibrarySession
private val librarySessionCallback = CustomMediaLibrarySessionCallback() private lateinit var customCommands: List<CommandButton>
private var customLayout = ImmutableList.of<CommandButton>()
companion object { companion object {
private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch" private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch"
private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" 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 { 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<SessionResult> {
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( override fun onGetLibraryRoot(
session: MediaLibrarySession, session: MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: ControllerInfo,
params: LibraryParams? params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> { ): ListenableFuture<LibraryResult<MediaItem>> {
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params)) return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
@ -54,7 +139,7 @@ class PlaybackService : MediaLibraryService() {
override fun onGetItem( override fun onGetItem(
session: MediaLibrarySession, session: MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: ControllerInfo,
mediaId: String mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> { ): ListenableFuture<LibraryResult<MediaItem>> {
val item = val item =
@ -65,9 +150,24 @@ class PlaybackService : MediaLibraryService() {
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null))
} }
override fun onSubscribe(
session: MediaLibrarySession,
browser: ControllerInfo,
parentId: String,
params: LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
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( override fun onGetChildren(
session: MediaLibrarySession, session: MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: ControllerInfo,
parentId: String, parentId: String,
page: Int, page: Int,
pageSize: Int, pageSize: Int,
@ -82,6 +182,26 @@ class PlaybackService : MediaLibraryService() {
return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) 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) { private fun setMediaItemFromSearchQuery(query: String) {
// Only accept query with pattern "play [Title]" or "[Title]" // Only accept query with pattern "play [Title]" or "[Title]"
// Where [Title]: must be exactly matched // Where [Title]: must be exactly matched
@ -96,66 +216,6 @@ class PlaybackService : MediaLibraryService() {
val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem()
player.setMediaItem(item) 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<LibraryResult<Void>> {
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() { private fun initializeSessionAndPlayer() {
@ -165,13 +225,10 @@ class PlaybackService : MediaLibraryService() {
.build() .build()
MediaItemTree.initialize(assets) MediaItemTree.initialize(assets)
val parentScreenIntent = Intent(this, MainActivity::class.java) val sessionActivityPendingIntent =
val intent = Intent(this, PlayerActivity::class.java)
val pendingIntent =
TaskStackBuilder.create(this).run { TaskStackBuilder.create(this).run {
addNextIntent(parentScreenIntent) addNextIntent(Intent(this@PlaybackService, MainActivity::class.java))
addNextIntent(intent) addNextIntent(Intent(this@PlaybackService, PlayerActivity::class.java))
val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0 val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT) getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
@ -180,7 +237,39 @@ class PlaybackService : MediaLibraryService() {
mediaLibrarySession = mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback) MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setMediaItemFiller(CustomMediaItemFiller()) .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() .build()
} }
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */
}
private class CustomMediaItemFiller : MediaSession.MediaItemFiller {
override fun fillInLocalConfiguration(
session: MediaSession,
controller: ControllerInfo,
mediaItem: MediaItem
): MediaItem {
return MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem
}
}
} }