Demonstrate how to use custom actions in the session demo app.
PiperOrigin-RevId: 451410714
This commit is contained in:
parent
6c4f6ecf46
commit
9a70bbca05
@ -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()
|
.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<SessionResult>) {
|
||||||
|
/* Do nothing. */
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomMediaItemFiller : MediaSession.MediaItemFiller {
|
||||||
|
override fun fillInLocalConfiguration(
|
||||||
|
session: MediaSession,
|
||||||
|
controller: ControllerInfo,
|
||||||
|
mediaItem: MediaItem
|
||||||
|
): MediaItem {
|
||||||
|
return MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user