mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-18 18:17:43 +03:00
Merge branch 'feature/android-auto-shuffle-repeat' into 'develop'
Added custom buttons for shuffling the current queue and setting repeat mode Closes #883 See merge request ultrasonic/ultrasonic!1075
This commit is contained in:
commit
acbaae9f14
@ -17,6 +17,7 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS
|
|||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
|
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
|
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Rating
|
import androidx.media3.common.Rating
|
||||||
import androidx.media3.common.StarRating
|
import androidx.media3.common.StarRating
|
||||||
import androidx.media3.session.CommandButton
|
import androidx.media3.session.CommandButton
|
||||||
@ -116,22 +117,59 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
private val isOffline get() = ActiveServerProvider.isOffline()
|
private val isOffline get() = ActiveServerProvider.isOffline()
|
||||||
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
||||||
|
|
||||||
private var customCommands: List<CommandButton>
|
private val placeholderButton = getPlaceholderButton()
|
||||||
internal var customLayout = ImmutableList.of<CommandButton>()
|
|
||||||
|
private var heartIsCurrentlyOn = false
|
||||||
|
|
||||||
|
// This button is used for an unstarred track, and its action will star the track
|
||||||
|
private val heartButtonToggleOn =
|
||||||
|
getHeartCommandButton(
|
||||||
|
SessionCommand(
|
||||||
|
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON,
|
||||||
|
Bundle.EMPTY
|
||||||
|
),
|
||||||
|
willHeart = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// This button is used for an starred track, and its action will star the track
|
||||||
|
private val heartButtonToggleOff =
|
||||||
|
getHeartCommandButton(
|
||||||
|
SessionCommand(
|
||||||
|
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF,
|
||||||
|
Bundle.EMPTY
|
||||||
|
),
|
||||||
|
willHeart = false
|
||||||
|
)
|
||||||
|
|
||||||
|
private val shuffleButton: CommandButton
|
||||||
|
|
||||||
|
private val repeatOffButton: CommandButton
|
||||||
|
private val repeatOneButton: CommandButton
|
||||||
|
private val repeatAllButton: CommandButton
|
||||||
|
|
||||||
|
private val allCustomCommands: List<CommandButton>
|
||||||
|
|
||||||
|
val defaultCustomCommands: List<CommandButton>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
customCommands =
|
val shuffleCommand = SessionCommand(PlaybackService.CUSTOM_COMMAND_SHUFFLE, Bundle.EMPTY)
|
||||||
listOf(
|
shuffleButton = getShuffleCommandButton(shuffleCommand)
|
||||||
// This button is used for an unstarred track, and its action will star the track
|
|
||||||
getHeartCommandButton(
|
val repeatCommand = SessionCommand(PlaybackService.CUSTOM_COMMAND_REPEAT_MODE, Bundle.EMPTY)
|
||||||
SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON, Bundle.EMPTY)
|
repeatOffButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_OFF)
|
||||||
),
|
repeatOneButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_ONE)
|
||||||
// This button is used for an starred track, and its action will unstar the track
|
repeatAllButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_ALL)
|
||||||
getHeartCommandButton(
|
|
||||||
SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF, Bundle.EMPTY)
|
allCustomCommands = listOf(
|
||||||
)
|
heartButtonToggleOn,
|
||||||
)
|
heartButtonToggleOff,
|
||||||
customLayout = ImmutableList.of(customCommands[0])
|
shuffleButton,
|
||||||
|
repeatOffButton,
|
||||||
|
repeatOneButton,
|
||||||
|
repeatAllButton
|
||||||
|
)
|
||||||
|
|
||||||
|
defaultCustomCommands = listOf(heartButtonToggleOn, shuffleButton, repeatOffButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -188,14 +226,17 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
controller: MediaSession.ControllerInfo
|
controller: MediaSession.ControllerInfo
|
||||||
): MediaSession.ConnectionResult {
|
): MediaSession.ConnectionResult {
|
||||||
Timber.i("onConnect")
|
Timber.i("onConnect")
|
||||||
|
|
||||||
val connectionResult = super.onConnect(session, controller)
|
val connectionResult = super.onConnect(session, controller)
|
||||||
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||||
|
|
||||||
for (commandButton in customCommands) {
|
for (commandButton in allCustomCommands) {
|
||||||
// Add custom command to available session commands.
|
// Add custom command to available session commands.
|
||||||
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.player.repeatMode = Player.REPEAT_MODE_ALL
|
||||||
|
|
||||||
return MediaSession.ConnectionResult.accept(
|
return MediaSession.ConnectionResult.accept(
|
||||||
availableSessionCommands.build(),
|
availableSessionCommands.build(),
|
||||||
connectionResult.availablePlayerCommands
|
connectionResult.availablePlayerCommands
|
||||||
@ -203,26 +244,72 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
||||||
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
|
if (controller.controllerVersion != 0) {
|
||||||
// Let Media3 controller (for instance the MediaNotificationProvider)
|
// Let Media3 controller (for instance the MediaNotificationProvider)
|
||||||
// know about the custom layout right after it connected.
|
// know about the custom layout right after it connected.
|
||||||
session.setCustomLayout(customLayout)
|
with(session) {
|
||||||
|
setCustomLayout(session.buildCustomCommands(canShuffle = canShuffle()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHeartCommandButton(sessionCommand: SessionCommand): CommandButton {
|
private fun getHeartCommandButton(sessionCommand: SessionCommand, willHeart: Boolean) =
|
||||||
val willHeart =
|
CommandButton.Builder()
|
||||||
(sessionCommand.customAction == PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON)
|
.setDisplayName(
|
||||||
return CommandButton.Builder()
|
if (willHeart)
|
||||||
.setDisplayName("Love")
|
"Love"
|
||||||
|
else
|
||||||
|
"Dislike"
|
||||||
|
)
|
||||||
.setIconResId(
|
.setIconResId(
|
||||||
if (willHeart) R.drawable.ic_star_hollow
|
if (willHeart)
|
||||||
else R.drawable.ic_star_full
|
R.drawable.ic_star_hollow
|
||||||
|
else
|
||||||
|
R.drawable.ic_star_full
|
||||||
|
)
|
||||||
|
.setSessionCommand(sessionCommand)
|
||||||
|
.setEnabled(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun getShuffleCommandButton(sessionCommand: SessionCommand) =
|
||||||
|
CommandButton.Builder()
|
||||||
|
.setDisplayName("Shuffle")
|
||||||
|
.setIconResId(R.drawable.media_shuffle)
|
||||||
|
.setSessionCommand(sessionCommand)
|
||||||
|
.setEnabled(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun getPlaceholderButton() = CommandButton.Builder()
|
||||||
|
.setDisplayName("Placeholder")
|
||||||
|
.setIconResId(R.drawable.empty)
|
||||||
|
.setSessionCommand(
|
||||||
|
SessionCommand(
|
||||||
|
PlaybackService.CUSTOM_COMMAND_PLACEHOLDER,
|
||||||
|
Bundle.EMPTY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setEnabled(false)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun getRepeatModeButton(sessionCommand: SessionCommand, repeatMode: Int) =
|
||||||
|
CommandButton.Builder()
|
||||||
|
.setDisplayName(
|
||||||
|
when (repeatMode) {
|
||||||
|
Player.REPEAT_MODE_ONE -> "Repeat One"
|
||||||
|
Player.REPEAT_MODE_ALL -> "Repeat All"
|
||||||
|
else -> "Repeat None"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.setIconResId(
|
||||||
|
when (repeatMode) {
|
||||||
|
Player.REPEAT_MODE_ONE -> R.drawable.media_repeat_one
|
||||||
|
Player.REPEAT_MODE_ALL -> R.drawable.media_repeat_all
|
||||||
|
else -> R.drawable.media_repeat_off
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.setSessionCommand(sessionCommand)
|
.setSessionCommand(sessionCommand)
|
||||||
.setEnabled(true)
|
.setEnabled(true)
|
||||||
.build()
|
.build()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetItem(
|
override fun onGetItem(
|
||||||
session: MediaLibraryService.MediaLibrarySession,
|
session: MediaLibraryService.MediaLibrarySession,
|
||||||
@ -266,18 +353,30 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
customCommand: SessionCommand,
|
customCommand: SessionCommand,
|
||||||
args: Bundle
|
args: Bundle
|
||||||
): ListenableFuture<SessionResult> {
|
): ListenableFuture<SessionResult> {
|
||||||
Timber.i("onCustomCommand")
|
Timber.i("onCustomCommand %s", customCommand.customAction)
|
||||||
var customCommandFuture: ListenableFuture<SessionResult>? = null
|
var customCommandFuture: ListenableFuture<SessionResult>? = null
|
||||||
|
|
||||||
when (customCommand.customAction) {
|
when (customCommand.customAction) {
|
||||||
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON -> {
|
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON -> {
|
||||||
customCommandFuture = onSetRating(session, controller, HeartRating(true))
|
customCommandFuture = onSetRating(session, controller, HeartRating(true))
|
||||||
updateCustomHeartButton(session, true)
|
updateCustomHeartButton(session, isHeart = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> {
|
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> {
|
||||||
customCommandFuture = onSetRating(session, controller, HeartRating(false))
|
customCommandFuture = onSetRating(session, controller, HeartRating(false))
|
||||||
updateCustomHeartButton(session, false)
|
updateCustomHeartButton(session, isHeart = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackService.CUSTOM_COMMAND_SHUFFLE -> {
|
||||||
|
customCommandFuture = Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
|
||||||
|
shuffleCurrentPlaylist(session.player)
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackService.CUSTOM_COMMAND_REPEAT_MODE -> {
|
||||||
|
customCommandFuture = Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
|
||||||
|
|
||||||
|
session.player.setNextRepeatMode()
|
||||||
|
session.updateCustomCommands()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@ -288,9 +387,14 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (customCommandFuture != null)
|
|
||||||
return customCommandFuture
|
return customCommandFuture
|
||||||
return super.onCustomCommand(session, controller, customCommand, args)
|
?: super.onCustomCommand(
|
||||||
|
session,
|
||||||
|
controller,
|
||||||
|
customCommand,
|
||||||
|
args
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetRating(
|
override fun onSetRating(
|
||||||
@ -299,6 +403,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
rating: Rating
|
rating: Rating
|
||||||
): ListenableFuture<SessionResult> {
|
): ListenableFuture<SessionResult> {
|
||||||
val mediaItem = session.player.currentMediaItem
|
val mediaItem = session.player.currentMediaItem
|
||||||
|
|
||||||
if (mediaItem != null) {
|
if (mediaItem != null) {
|
||||||
if (rating is HeartRating) {
|
if (rating is HeartRating) {
|
||||||
mediaItem.toTrack().starred = rating.isHeart
|
mediaItem.toTrack().starred = rating.isHeart
|
||||||
@ -312,6 +417,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
rating
|
rating
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onSetRating(session, controller, rating)
|
return super.onSetRating(session, controller, rating)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,6 +487,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
private fun onAddLegacyAutoItems(
|
private fun onAddLegacyAutoItems(
|
||||||
mediaItems: MutableList<MediaItem>
|
mediaItems: MutableList<MediaItem>
|
||||||
): ListenableFuture<List<MediaItem>> {
|
): ListenableFuture<List<MediaItem>> {
|
||||||
|
Timber.i("onAddLegacyAutoItems %s", mediaItems.first().mediaId)
|
||||||
|
|
||||||
val mediaIdParts = mediaItems.first().mediaId.split('|')
|
val mediaIdParts = mediaItems.first().mediaId.split('|')
|
||||||
|
|
||||||
val tracks = when (mediaIdParts.first()) {
|
val tracks = when (mediaIdParts.first()) {
|
||||||
@ -410,55 +518,54 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracks != null) {
|
return tracks
|
||||||
return Futures.immediateFuture(
|
?.let {
|
||||||
tracks.map { track -> track.toMediaItem() }
|
Futures.immediateFuture(
|
||||||
.toMutableList()
|
it.map { track -> track.toMediaItem() }
|
||||||
)
|
.toMutableList()
|
||||||
}
|
)
|
||||||
|
}
|
||||||
// Fallback to the original list
|
?: Futures.immediateFuture(mediaItems)
|
||||||
return Futures.immediateFuture(mediaItems)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ReturnCount", "ComplexMethod")
|
@Suppress("ComplexMethod")
|
||||||
fun onLoadChildren(
|
private fun onLoadChildren(
|
||||||
parentId: String,
|
parentId: String,
|
||||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||||
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
|
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
|
||||||
|
|
||||||
val parentIdParts = parentId.split('|')
|
val parentIdParts = parentId.split('|')
|
||||||
|
|
||||||
when (parentIdParts.first()) {
|
return when (parentIdParts.first()) {
|
||||||
MEDIA_ROOT_ID -> return getRootItems()
|
MEDIA_ROOT_ID -> getRootItems()
|
||||||
MEDIA_LIBRARY_ID -> return getLibrary()
|
MEDIA_LIBRARY_ID -> getLibrary()
|
||||||
MEDIA_ARTIST_ID -> return getArtists()
|
MEDIA_ARTIST_ID -> getArtists()
|
||||||
MEDIA_ARTIST_SECTION -> return getArtists(parentIdParts[1])
|
MEDIA_ARTIST_SECTION -> getArtists(parentIdParts[1])
|
||||||
MEDIA_ALBUM_ID -> return getAlbums(AlbumListType.SORTED_BY_NAME)
|
MEDIA_ALBUM_ID -> getAlbums(AlbumListType.SORTED_BY_NAME)
|
||||||
MEDIA_ALBUM_PAGE_ID -> return getAlbums(
|
MEDIA_ALBUM_PAGE_ID -> getAlbums(
|
||||||
AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt()
|
AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_PLAYLIST_ID -> return getPlaylists()
|
MEDIA_PLAYLIST_ID -> getPlaylists()
|
||||||
MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(AlbumListType.FREQUENT)
|
MEDIA_ALBUM_FREQUENT_ID -> getAlbums(AlbumListType.FREQUENT)
|
||||||
MEDIA_ALBUM_NEWEST_ID -> return getAlbums(AlbumListType.NEWEST)
|
MEDIA_ALBUM_NEWEST_ID -> getAlbums(AlbumListType.NEWEST)
|
||||||
MEDIA_ALBUM_RECENT_ID -> return getAlbums(AlbumListType.RECENT)
|
MEDIA_ALBUM_RECENT_ID -> getAlbums(AlbumListType.RECENT)
|
||||||
MEDIA_ALBUM_RANDOM_ID -> return getAlbums(AlbumListType.RANDOM)
|
MEDIA_ALBUM_RANDOM_ID -> getAlbums(AlbumListType.RANDOM)
|
||||||
MEDIA_ALBUM_STARRED_ID -> return getAlbums(AlbumListType.STARRED)
|
MEDIA_ALBUM_STARRED_ID -> getAlbums(AlbumListType.STARRED)
|
||||||
MEDIA_SONG_RANDOM_ID -> return getRandomSongs()
|
MEDIA_SONG_RANDOM_ID -> getRandomSongs()
|
||||||
MEDIA_SONG_STARRED_ID -> return getStarredSongs()
|
MEDIA_SONG_STARRED_ID -> getStarredSongs()
|
||||||
MEDIA_SHARE_ID -> return getShares()
|
MEDIA_SHARE_ID -> getShares()
|
||||||
MEDIA_BOOKMARK_ID -> return getBookmarks()
|
MEDIA_BOOKMARK_ID -> getBookmarks()
|
||||||
MEDIA_PODCAST_ID -> return getPodcasts()
|
MEDIA_PODCAST_ID -> getPodcasts()
|
||||||
MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2])
|
MEDIA_PLAYLIST_ITEM -> getPlaylist(parentIdParts[1], parentIdParts[2])
|
||||||
MEDIA_ARTIST_ITEM -> return getAlbumsForArtist(
|
MEDIA_ARTIST_ITEM -> getAlbumsForArtist(
|
||||||
parentIdParts[1], parentIdParts[2]
|
parentIdParts[1], parentIdParts[2]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_ALBUM_ITEM -> return getSongsForAlbum(parentIdParts[1], parentIdParts[2])
|
MEDIA_ALBUM_ITEM -> getSongsForAlbum(parentIdParts[1], parentIdParts[2])
|
||||||
MEDIA_SHARE_ITEM -> return getSongsForShare(parentIdParts[1])
|
MEDIA_SHARE_ITEM -> getSongsForShare(parentIdParts[1])
|
||||||
MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(parentIdParts[1])
|
MEDIA_PODCAST_ITEM -> getPodcastEpisodes(parentIdParts[1])
|
||||||
else -> return Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null))
|
else -> Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1316,14 +1423,102 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCustomHeartButton(
|
private fun Player.setNextRepeatMode() {
|
||||||
session: MediaSession,
|
repeatMode =
|
||||||
isHeart: Boolean
|
when (repeatMode) {
|
||||||
) {
|
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL
|
||||||
val command = if (isHeart) customCommands[1] else customCommands[0]
|
Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE
|
||||||
// Change the custom layout to contain the right heart button
|
else -> Player.REPEAT_MODE_OFF
|
||||||
customLayout = ImmutableList.of(command)
|
}
|
||||||
// Send the updated custom layout to controllers.
|
}
|
||||||
session.setCustomLayout(customLayout)
|
|
||||||
|
private fun MediaSession.updateCustomCommands() {
|
||||||
|
setCustomLayout(
|
||||||
|
buildCustomCommands(
|
||||||
|
heartIsCurrentlyOn,
|
||||||
|
canShuffle()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCustomHeartButton(session: MediaSession, isHeart: Boolean) {
|
||||||
|
with(session) {
|
||||||
|
setCustomLayout(
|
||||||
|
buildCustomCommands(
|
||||||
|
isHeart = isHeart,
|
||||||
|
canShuffle = canShuffle()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MediaSession.canShuffle() =
|
||||||
|
player.mediaItemCount > 2
|
||||||
|
|
||||||
|
private fun MediaSession.buildCustomCommands(
|
||||||
|
isHeart: Boolean = false,
|
||||||
|
canShuffle: Boolean = false
|
||||||
|
): ImmutableList<CommandButton> {
|
||||||
|
Timber.d("building custom commands (isHeart = %s, canShuffle = %s)", isHeart, canShuffle)
|
||||||
|
|
||||||
|
heartIsCurrentlyOn = isHeart
|
||||||
|
|
||||||
|
return ImmutableList.copyOf(
|
||||||
|
buildList {
|
||||||
|
// placeholder must come first here because if there is no next button the first
|
||||||
|
// custom command button is place right next to the play/pause button
|
||||||
|
if (
|
||||||
|
player.repeatMode != Player.REPEAT_MODE_ALL &&
|
||||||
|
player.currentMediaItemIndex == player.mediaItemCount - 1
|
||||||
|
)
|
||||||
|
add(placeholderButton)
|
||||||
|
|
||||||
|
// due to the previous placeholder this heart button will always appear to the left
|
||||||
|
// of the default playback items
|
||||||
|
add(
|
||||||
|
if (isHeart)
|
||||||
|
heartButtonToggleOff
|
||||||
|
else
|
||||||
|
heartButtonToggleOn
|
||||||
|
)
|
||||||
|
|
||||||
|
// both the shuffle and the active repeat mode button will end up in the overflow
|
||||||
|
// menu if both are available at the same time
|
||||||
|
if (canShuffle)
|
||||||
|
add(shuffleButton)
|
||||||
|
|
||||||
|
add(
|
||||||
|
when (player.repeatMode) {
|
||||||
|
Player.REPEAT_MODE_ONE -> repeatOneButton
|
||||||
|
Player.REPEAT_MODE_ALL -> repeatAllButton
|
||||||
|
else -> repeatOffButton
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.asIterable()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shuffleCurrentPlaylist(player: Player) {
|
||||||
|
Timber.d("shuffleCurrentPlaylist")
|
||||||
|
|
||||||
|
// 3 was chosen because that leaves at least two other songs to be shuffled around
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
if (player.mediaItemCount < 3)
|
||||||
|
return
|
||||||
|
|
||||||
|
val mediaItemsToShuffle = mutableListOf<MediaItem>()
|
||||||
|
|
||||||
|
for (i in 0 until player.currentMediaItemIndex) {
|
||||||
|
mediaItemsToShuffle += player.getMediaItemAt(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in player.currentMediaItemIndex + 1 until player.mediaItemCount) {
|
||||||
|
mediaItemsToShuffle += player.getMediaItemAt(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
player.removeMediaItems(player.currentMediaItemIndex + 1, player.mediaItemCount)
|
||||||
|
player.removeMediaItems(0, player.currentMediaItemIndex)
|
||||||
|
|
||||||
|
player.addMediaItems(mediaItemsToShuffle.shuffled())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,10 +150,8 @@ class PlaybackService :
|
|||||||
.setBitmapLoader(ArtworkBitmapLoader())
|
.setBitmapLoader(ArtworkBitmapLoader())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
if (!librarySessionCallback.customLayout.isEmpty()) {
|
// Send custom layout to legacy session.
|
||||||
// Send custom layout to legacy session.
|
mediaLibrarySession.setCustomLayout(librarySessionCallback.defaultCustomCommands)
|
||||||
mediaLibrarySession.setCustomLayout(librarySessionCallback.customLayout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a listener to update the API client when the active server has changed
|
// Set a listener to update the API client when the active server has changed
|
||||||
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
||||||
@ -422,6 +420,12 @@ class PlaybackService :
|
|||||||
"org.moire.ultrasonic.HEART_ON"
|
"org.moire.ultrasonic.HEART_ON"
|
||||||
const val CUSTOM_COMMAND_TOGGLE_HEART_OFF =
|
const val CUSTOM_COMMAND_TOGGLE_HEART_OFF =
|
||||||
"org.moire.ultrasonic.HEART_OFF"
|
"org.moire.ultrasonic.HEART_OFF"
|
||||||
|
const val CUSTOM_COMMAND_SHUFFLE =
|
||||||
|
"org.moire.ultrasonic.SHUFFLE"
|
||||||
|
const val CUSTOM_COMMAND_PLACEHOLDER =
|
||||||
|
"org.moire.ultrasonic.PLACEHOLDER"
|
||||||
|
const val CUSTOM_COMMAND_REPEAT_MODE =
|
||||||
|
"org.moire.ultrasonic.REPEAT_MODE"
|
||||||
private const val NOTIFICATION_ID = 3009
|
private const val NOTIFICATION_ID = 3009
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
ultrasonic/src/main/res/drawable/empty.xml
Normal file
7
ultrasonic/src/main/res/drawable/empty.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<size
|
||||||
|
android:width="120dp"
|
||||||
|
android:height="120dp"/>
|
||||||
|
</shape>
|
Loading…
x
Reference in New Issue
Block a user