ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt

1106 lines
42 KiB
Kotlin

/*
* AutoMediaBrowserService.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.service
import android.os.Bundle
import android.os.Handler
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.session.MediaSessionCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants
import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.util.MediaSessionEventDistributor
import org.moire.ultrasonic.util.MediaSessionEventListener
import org.moire.ultrasonic.util.MediaSessionHandler
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import timber.log.Timber
private const val MEDIA_ROOT_ID = "MEDIA_ROOT_ID"
private const val MEDIA_ALBUM_ID = "MEDIA_ALBUM_ID"
private const val MEDIA_ALBUM_PAGE_ID = "MEDIA_ALBUM_PAGE_ID"
private const val MEDIA_ALBUM_NEWEST_ID = "MEDIA_ALBUM_NEWEST_ID"
private const val MEDIA_ALBUM_RECENT_ID = "MEDIA_ALBUM_RECENT_ID"
private const val MEDIA_ALBUM_FREQUENT_ID = "MEDIA_ALBUM_FREQUENT_ID"
private const val MEDIA_ALBUM_RANDOM_ID = "MEDIA_ALBUM_RANDOM_ID"
private const val MEDIA_ALBUM_STARRED_ID = "MEDIA_ALBUM_STARRED_ID"
private const val MEDIA_SONG_RANDOM_ID = "MEDIA_SONG_RANDOM_ID"
private const val MEDIA_SONG_STARRED_ID = "MEDIA_SONG_STARRED_ID"
private const val MEDIA_ARTIST_ID = "MEDIA_ARTIST_ID"
private const val MEDIA_LIBRARY_ID = "MEDIA_LIBRARY_ID"
private const val MEDIA_PLAYLIST_ID = "MEDIA_PLAYLIST_ID"
private const val MEDIA_SHARE_ID = "MEDIA_SHARE_ID"
private const val MEDIA_BOOKMARK_ID = "MEDIA_BOOKMARK_ID"
private const val MEDIA_PODCAST_ID = "MEDIA_PODCAST_ID"
private const val MEDIA_ALBUM_ITEM = "MEDIA_ALBUM_ITEM"
private const val MEDIA_PLAYLIST_SONG_ITEM = "MEDIA_PLAYLIST_SONG_ITEM"
private const val MEDIA_PLAYLIST_ITEM = "MEDIA_PLAYLIST_ITEM"
private const val MEDIA_ARTIST_ITEM = "MEDIA_ARTIST_ITEM"
private const val MEDIA_ARTIST_SECTION = "MEDIA_ARTIST_SECTION"
private const val MEDIA_ALBUM_SONG_ITEM = "MEDIA_ALBUM_SONG_ITEM"
private const val MEDIA_SONG_STARRED_ITEM = "MEDIA_SONG_STARRED_ITEM"
private const val MEDIA_SONG_RANDOM_ITEM = "MEDIA_SONG_RANDOM_ITEM"
private const val MEDIA_SHARE_ITEM = "MEDIA_SHARE_ITEM"
private const val MEDIA_SHARE_SONG_ITEM = "MEDIA_SHARE_SONG_ITEM"
private const val MEDIA_BOOKMARK_ITEM = "MEDIA_BOOKMARK_ITEM"
private const val MEDIA_PODCAST_ITEM = "MEDIA_PODCAST_ITEM"
private const val MEDIA_PODCAST_EPISODE_ITEM = "MEDIA_PODCAST_EPISODE_ITEM"
private const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM"
// Currently the display limit for long lists is 100 items
private const val DISPLAY_LIMIT = 100
private const val SEARCH_LIMIT = 10
/**
* MediaBrowserService implementation for e.g. Android Auto
*/
@Suppress("TooManyFunctions", "LargeClass")
class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private lateinit var mediaSessionEventListener: MediaSessionEventListener
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>()
private val mediaSessionHandler by inject<MediaSessionHandler>()
private val mediaPlayerController by inject<MediaPlayerController>()
private val activeServerProvider: ActiveServerProvider by inject()
private val musicService = MusicServiceFactory.getMusicService()
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private var playlistCache: List<MusicDirectory.Entry>? = null
private var starredSongsCache: List<MusicDirectory.Entry>? = null
private var randomSongsCache: List<MusicDirectory.Entry>? = null
private var searchSongsCache: List<MusicDirectory.Entry>? = null
private val isOffline get() = ActiveServerProvider.isOffline()
private val useId3Tags get() = Settings.shouldUseId3Tags
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
private var mediaSessionTokenSubscription: Disposable? = null
@Suppress("MagicNumber")
override fun onCreate() {
super.onCreate()
mediaSessionTokenSubscription = RxBus.mediaSessionTokenObservable.subscribe {
if (sessionToken == null) sessionToken = it
}
mediaSessionEventListener = object : MediaSessionEventListener {
override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) {
Timber.d(
"AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s",
mediaId
)
if (mediaId == null) return
val mediaIdParts = mediaId.split('|')
when (mediaIdParts.first()) {
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
)
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
)
MEDIA_SONG_STARRED_ID -> playStarredSongs()
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
MEDIA_SONG_RANDOM_ITEM -> playRandomSong(mediaIdParts[1])
MEDIA_SHARE_ITEM -> playShare(mediaIdParts[1])
MEDIA_SHARE_SONG_ITEM -> playShareSong(mediaIdParts[1], mediaIdParts[2])
MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1])
MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1])
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
mediaIdParts[1], mediaIdParts[2]
)
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
}
}
override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {
Timber.d("AutoMediaBrowserService onPlayFromSearchRequested query: %s", query)
if (query.isNullOrBlank()) playRandomSongs()
serviceScope.launch {
val criteria = SearchCriteria(query!!, 0, 0, DISPLAY_LIMIT)
val searchResult = callWithErrorHandling { musicService.search(criteria) }
// Try to find the best match
if (searchResult != null) {
val song = searchResult.songs
.asSequence()
.sortedByDescending { song -> song.starred }
.sortedByDescending { song -> song.averageRating }
.sortedByDescending { song -> song.userRating }
.sortedByDescending { song -> song.closeness }
.firstOrNull()
if (song != null) playSong(song)
}
}
}
}
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
mediaSessionHandler.initialize()
val handler = Handler()
handler.postDelayed(
{
// Ultrasonic may be started from Android Auto. This boots up the necessary components.
Timber.d(
"AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService..."
)
lifecycleSupport.onCreate()
MediaPlayerService.getInstance()
},
100
)
Timber.i("AutoMediaBrowserService onCreate finished")
}
override fun onDestroy() {
super.onDestroy()
mediaSessionTokenSubscription?.dispose()
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
mediaSessionHandler.release()
serviceJob.cancel()
Timber.i("AutoMediaBrowserService onDestroy finished")
}
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): BrowserRoot {
Timber.d(
"AutoMediaBrowserService onGetRoot called. clientPackageName: %s; clientUid: %d",
clientPackageName, clientUid
)
val extras = Bundle()
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
)
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
)
extras.putBoolean(
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
)
return BrowserRoot(MEDIA_ROOT_ID, extras)
}
@Suppress("ReturnCount", "ComplexMethod")
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
val parentIdParts = parentId.split('|')
when (parentIdParts.first()) {
MEDIA_ROOT_ID -> return getRootItems(result)
MEDIA_LIBRARY_ID -> return getLibrary(result)
MEDIA_ARTIST_ID -> return getArtists(result)
MEDIA_ARTIST_SECTION -> return getArtists(result, parentIdParts[1])
MEDIA_ALBUM_ID -> return getAlbums(result, AlbumListType.SORTED_BY_NAME)
MEDIA_ALBUM_PAGE_ID -> return getAlbums(
result, AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt()
)
MEDIA_PLAYLIST_ID -> return getPlaylists(result)
MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(result, AlbumListType.FREQUENT)
MEDIA_ALBUM_NEWEST_ID -> return getAlbums(result, AlbumListType.NEWEST)
MEDIA_ALBUM_RECENT_ID -> return getAlbums(result, AlbumListType.RECENT)
MEDIA_ALBUM_RANDOM_ID -> return getAlbums(result, AlbumListType.RANDOM)
MEDIA_ALBUM_STARRED_ID -> return getAlbums(result, AlbumListType.STARRED)
MEDIA_SONG_RANDOM_ID -> return getRandomSongs(result)
MEDIA_SONG_STARRED_ID -> return getStarredSongs(result)
MEDIA_SHARE_ID -> return getShares(result)
MEDIA_BOOKMARK_ID -> return getBookmarks(result)
MEDIA_PODCAST_ID -> return getPodcasts(result)
MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2], result)
MEDIA_ARTIST_ITEM -> return getAlbumsForArtist(
result, parentIdParts[1], parentIdParts[2]
)
MEDIA_ALBUM_ITEM -> return getSongsForAlbum(result, parentIdParts[1], parentIdParts[2])
MEDIA_SHARE_ITEM -> return getSongsForShare(result, parentIdParts[1])
MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(result, parentIdParts[1])
else -> result.sendResult(mutableListOf())
}
}
override fun onSearch(
query: String,
extras: Bundle?,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
Timber.d("AutoMediaBrowserService onSearch query: %s", query)
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val criteria = SearchCriteria(query, SEARCH_LIMIT, SEARCH_LIMIT, SEARCH_LIMIT)
val searchResult = callWithErrorHandling { musicService.search(criteria) }
// TODO Add More... button to categories
if (searchResult != null) {
searchResult.artists.map { artist ->
mediaItems.add(
artist.name ?: "",
listOf(MEDIA_ARTIST_ITEM, artist.id, artist.name).joinToString("|"),
null,
R.string.search_artists
)
}
searchResult.albums.map { album ->
mediaItems.add(
album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
.joinToString("|"),
null,
R.string.search_albums
)
}
searchSongsCache = searchResult.songs
searchResult.songs.map { song ->
mediaItems.add(
MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
song,
listOf(MEDIA_SEARCH_SONG_ITEM, song.id).joinToString("|"),
R.string.search_songs
),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
}
result.sendResult(mediaItems)
}
}
private fun playSearch(id: String) {
serviceScope.launch {
// If there is no cache, we can't play the selected song.
if (searchSongsCache != null) {
val song = searchSongsCache!!.firstOrNull { x -> x.id == id }
if (song != null) playSong(song)
}
}
}
private fun getRootItems(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
if (!isOffline)
mediaItems.add(
R.string.music_library_label,
MEDIA_LIBRARY_ID,
R.drawable.ic_library,
null
)
mediaItems.add(
R.string.main_artists_title,
MEDIA_ARTIST_ID,
R.drawable.ic_artist,
null
)
if (!isOffline)
mediaItems.add(
R.string.main_albums_title,
MEDIA_ALBUM_ID,
R.drawable.ic_menu_browse_dark,
null
)
mediaItems.add(
R.string.playlist_label,
MEDIA_PLAYLIST_ID,
R.drawable.ic_menu_playlists_dark,
null
)
result.sendResult(mediaItems)
}
private fun getLibrary(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
// Songs
mediaItems.add(
R.string.main_songs_random,
MEDIA_SONG_RANDOM_ID,
null,
R.string.main_songs_title
)
mediaItems.add(
R.string.main_songs_starred,
MEDIA_SONG_STARRED_ID,
null,
R.string.main_songs_title
)
// Albums
mediaItems.add(
R.string.main_albums_newest,
MEDIA_ALBUM_NEWEST_ID,
null,
R.string.main_albums_title
)
mediaItems.add(
R.string.main_albums_recent,
MEDIA_ALBUM_RECENT_ID,
null,
R.string.main_albums_title
)
mediaItems.add(
R.string.main_albums_frequent,
MEDIA_ALBUM_FREQUENT_ID,
null,
R.string.main_albums_title
)
mediaItems.add(
R.string.main_albums_random,
MEDIA_ALBUM_RANDOM_ID,
null,
R.string.main_albums_title
)
mediaItems.add(
R.string.main_albums_starred,
MEDIA_ALBUM_STARRED_ID,
null,
R.string.main_albums_title
)
// Other
mediaItems.add(R.string.button_bar_shares, MEDIA_SHARE_ID, null, null)
mediaItems.add(R.string.button_bar_bookmarks, MEDIA_BOOKMARK_ID, null, null)
mediaItems.add(R.string.button_bar_podcasts, MEDIA_PODCAST_ID, null, null)
result.sendResult(mediaItems)
}
private fun getArtists(
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
section: String? = null
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val childMediaId: String
var artists = if (!isOffline && useId3Tags) {
childMediaId = MEDIA_ARTIST_ITEM
// TODO this list can be big so we're not refreshing.
// Maybe a refresh menu item can be added
callWithErrorHandling { musicService.getArtists(false) }
} else {
// This will be handled at getSongsForAlbum, which supports navigation
childMediaId = MEDIA_ALBUM_ITEM
callWithErrorHandling { musicService.getIndexes(musicFolderId, false) }
}
if (artists != null) {
if (section != null)
artists = artists.filter { artist ->
getSectionFromName(artist.name ?: "") == section
}
// If there are too many artists, create alphabetic index of them
if (section == null && artists.count() > DISPLAY_LIMIT) {
val index = mutableListOf<String>()
// TODO This sort should use ignoredArticles somehow...
artists = artists.sortedBy { artist -> artist.name }
artists.map { artist ->
val currentSection = getSectionFromName(artist.name ?: "")
if (!index.contains(currentSection)) {
index.add(currentSection)
mediaItems.add(
currentSection,
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"),
null
)
}
}
} else {
artists.map { artist ->
mediaItems.add(
artist.name ?: "",
listOf(childMediaId, artist.id, artist.name).joinToString("|"),
null
)
}
}
result.sendResult(mediaItems)
}
}
}
private fun getAlbumsForArtist(
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
id: String,
name: String
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val albums = if (!isOffline && useId3Tags) {
callWithErrorHandling { musicService.getArtist(id, name, false) }
} else {
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) }
}
albums?.getAllChild()?.map { album ->
mediaItems.add(
album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
.joinToString("|"),
null
)
}
result.sendResult(mediaItems)
}
}
private fun getSongsForAlbum(
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
id: String,
name: String
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val songs = listSongsInMusicService(id, name)
if (songs != null) {
if (songs.getChildren(includeDirs = true, includeFiles = false).count() == 0 &&
songs.getChildren(includeDirs = false, includeFiles = true).count() > 0
)
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all?
val items = songs.getChildren().take(DISPLAY_LIMIT)
items.map { item ->
if (item.isDirectory)
mediaItems.add(
MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
item,
listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|")
),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
else
mediaItems.add(
MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
item,
listOf(
MEDIA_ALBUM_SONG_ITEM,
id,
name,
item.id
).joinToString("|")
),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
}
result.sendResult(mediaItems)
}
}
private fun getAlbums(
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
type: AlbumListType,
page: Int? = null
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val offset = (page ?: 0) * DISPLAY_LIMIT
val albums = if (useId3Tags) {
callWithErrorHandling {
musicService.getAlbumList2(
type.typeName, DISPLAY_LIMIT, offset, null
)
}
} else {
callWithErrorHandling {
musicService.getAlbumList(
type.typeName, DISPLAY_LIMIT, offset, null
)
}
}
albums?.getAllChild()?.map { album ->
mediaItems.add(
album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
.joinToString("|"),
null
)
}
if (albums?.getAllChild()?.count() ?: 0 >= DISPLAY_LIMIT)
mediaItems.add(
R.string.search_more,
listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"),
R.drawable.ic_menu_forward_dark,
null
)
result.sendResult(mediaItems)
}
}
private fun getPlaylists(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val playlists = callWithErrorHandling { musicService.getPlaylists(true) }
playlists?.map { playlist ->
mediaItems.add(
playlist.name,
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
.joinToString("|"),
null
)
}
result.sendResult(mediaItems)
}
}
private fun getPlaylist(
id: String,
name: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
if (content != null) {
if (content.getAllChild().count() > 1)
mediaItems.addPlayAllItem(
listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|")
)
// Playlist should be cached as it may contain random elements
playlistCache = content.getAllChild()
playlistCache!!.take(DISPLAY_LIMIT).map { item ->
mediaItems.add(
MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
item,
listOf(
MEDIA_PLAYLIST_SONG_ITEM,
id,
name,
item.id
).joinToString("|")
),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
result.sendResult(mediaItems)
}
}
}
private fun playPlaylist(id: String, name: String) {
serviceScope.launch {
if (playlistCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
playlistCache = content?.getAllChild()
}
if (playlistCache != null) playSongs(playlistCache)
}
}
private fun playPlaylistSong(id: String, name: String, songId: String) {
serviceScope.launch {
if (playlistCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
playlistCache = content?.getAllChild()
}
val song = playlistCache?.firstOrNull { x -> x.id == songId }
if (song != null) playSong(song)
}
}
private fun playAlbum(id: String, name: String) {
serviceScope.launch {
val songs = listSongsInMusicService(id, name)
if (songs != null) playSongs(songs.getAllChild())
}
}
private fun playAlbumSong(id: String, name: String, songId: String) {
serviceScope.launch {
val songs = listSongsInMusicService(id, name)
val song = songs?.getAllChild()?.firstOrNull { x -> x.id == songId }
if (song != null) playSong(song)
}
}
private fun getPodcasts(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val podcasts = callWithErrorHandling { musicService.getPodcastsChannels(false) }
podcasts?.map { podcast ->
mediaItems.add(
podcast.title ?: "",
listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"),
null
)
}
result.sendResult(mediaItems)
}
}
private fun getPodcastEpisodes(
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
id: String
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) }
if (episodes != null) {
if (episodes.getAllChild().count() > 1)
mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|"))
episodes.getAllChild().map { episode ->
mediaItems.add(
MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
episode,
listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id)
.joinToString("|")
),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
result.sendResult(mediaItems)
}
}
}
private fun playPodcast(id: String) {
serviceScope.launch {
val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) }
if (episodes != null) {
playSongs(episodes.getAllChild())
}
}
}
private fun playPodcastEpisode(id: String, episodeId: String) {
serviceScope.launch {
val episodes = callWithErrorHandling { musicService.getPodcastEpisodes(id) }
if (episodes != null) {
val selectedEpisode = episodes
.getAllChild()
.firstOrNull { episode -> episode.id == episodeId }
if (selectedEpisode != null) playSong(selectedEpisode)
}
}
}
private fun getBookmarks(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val bookmarks = callWithErrorHandling { musicService.getBookmarks() }
if (bookmarks != null) {
val songs = Util.getSongsFromBookmarks(bookmarks)
songs.getAllChild().map { song ->
mediaItems.add(
MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
song,
listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|")
),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
result.sendResult(mediaItems)
}
}
}
private fun playBookmark(id: String) {
serviceScope.launch {
val bookmarks = callWithErrorHandling { musicService.getBookmarks() }
if (bookmarks != null) {
val songs = Util.getSongsFromBookmarks(bookmarks)
val song = songs.getAllChild().firstOrNull { song -> song.id == id }
if (song != null) playSong(song)
}
}
}
private fun getShares(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val shares = callWithErrorHandling { musicService.getShares(false) }
shares?.map { share ->
mediaItems.add(
share.name ?: "",
listOf(MEDIA_SHARE_ITEM, share.id)
.joinToString("|"),
null
)
}
result.sendResult(mediaItems)
}
}
private fun getSongsForShare(
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
id: String
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val shares = callWithErrorHandling { musicService.getShares(false) }
val selectedShare = shares?.firstOrNull { share -> share.id == id }
if (selectedShare != null) {
if (selectedShare.getEntries().count() > 1)
mediaItems.addPlayAllItem(listOf(MEDIA_SHARE_ITEM, id).joinToString("|"))
selectedShare.getEntries().map { song ->
mediaItems.add(
MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
song,
listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|")
),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
}
result.sendResult(mediaItems)
}
}
private fun playShare(id: String) {
serviceScope.launch {
val shares = callWithErrorHandling { musicService.getShares(false) }
val selectedShare = shares?.firstOrNull { share -> share.id == id }
if (selectedShare != null) {
playSongs(selectedShare.getEntries())
}
}
}
private fun playShareSong(id: String, songId: String) {
serviceScope.launch {
val shares = callWithErrorHandling { musicService.getShares(false) }
val selectedShare = shares?.firstOrNull { share -> share.id == id }
if (selectedShare != null) {
val song = selectedShare.getEntries().firstOrNull { x -> x.id == songId }
if (song != null) playSong(song)
}
}
}
private fun getStarredSongs(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val songs = listStarredSongsInMusicService()
if (songs != null) {
if (songs.songs.count() > 1)
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_STARRED_ID).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all?
val items = songs.songs.take(DISPLAY_LIMIT)
starredSongsCache = items
items.map { song ->
mediaItems.add(
MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
song,
listOf(MEDIA_SONG_STARRED_ITEM, song.id).joinToString("|")
),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
}
result.sendResult(mediaItems)
}
}
private fun playStarredSongs() {
serviceScope.launch {
if (starredSongsCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = listStarredSongsInMusicService()
starredSongsCache = content?.songs
}
if (starredSongsCache != null) playSongs(starredSongsCache)
}
}
private fun playStarredSong(songId: String) {
serviceScope.launch {
if (starredSongsCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = listStarredSongsInMusicService()
starredSongsCache = content?.songs
}
val song = starredSongsCache?.firstOrNull { x -> x.id == songId }
if (song != null) playSong(song)
}
}
private fun getRandomSongs(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) }
if (songs != null) {
if (songs.getAllChild().count() > 1)
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_RANDOM_ID).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all?
val items = songs.getAllChild()
randomSongsCache = items
items.map { song ->
mediaItems.add(
MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
song,
listOf(MEDIA_SONG_RANDOM_ITEM, song.id).joinToString("|")
),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
}
result.sendResult(mediaItems)
}
}
private fun playRandomSongs() {
serviceScope.launch {
if (randomSongsCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
// In this case we request a new set of random songs
val content = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) }
randomSongsCache = content?.getAllChild()
}
if (randomSongsCache != null) playSongs(randomSongsCache)
}
}
private fun playRandomSong(songId: String) {
serviceScope.launch {
// If there is no cache, we can't play the selected song.
if (randomSongsCache != null) {
val song = randomSongsCache!!.firstOrNull { x -> x.id == songId }
if (song != null) playSong(song)
}
}
}
private fun listSongsInMusicService(id: String, name: String): MusicDirectory? {
return if (!ActiveServerProvider.isOffline() && Settings.shouldUseId3Tags) {
callWithErrorHandling { musicService.getAlbum(id, name, false) }
} else {
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) }
}
}
private fun listStarredSongsInMusicService(): SearchResult? {
return if (Settings.shouldUseId3Tags) {
callWithErrorHandling { musicService.getStarred2() }
} else {
callWithErrorHandling { musicService.getStarred() }
}
}
private fun MutableList<MediaBrowserCompat.MediaItem>.add(
title: String,
mediaId: String,
icon: Int?,
groupNameId: Int? = null
) {
val builder = MediaDescriptionCompat.Builder()
builder.setTitle(title)
builder.setMediaId(mediaId)
if (icon != null)
builder.setIconUri(Util.getUriToDrawable(applicationContext, icon))
if (groupNameId != null)
builder.setExtras(
Bundle().apply {
putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
getString(groupNameId)
)
}
)
val mediaItem = MediaBrowserCompat.MediaItem(
builder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
this.add(mediaItem)
}
private fun MutableList<MediaBrowserCompat.MediaItem>.add(
resId: Int,
mediaId: String,
icon: Int?,
groupNameId: Int?,
browsable: Boolean = true
) {
val builder = MediaDescriptionCompat.Builder()
builder.setTitle(getString(resId))
builder.setMediaId(mediaId)
if (icon != null)
builder.setIconUri(Util.getUriToDrawable(applicationContext, icon))
if (groupNameId != null)
builder.setExtras(
Bundle().apply {
putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
getString(groupNameId)
)
}
)
val mediaItem = MediaBrowserCompat.MediaItem(
builder.build(),
if (browsable) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
this.add(mediaItem)
}
private fun MutableList<MediaBrowserCompat.MediaItem>.addPlayAllItem(
mediaId: String,
) {
this.add(
R.string.select_album_play_all,
mediaId,
R.drawable.ic_stat_play_dark,
null,
false
)
}
private fun getSectionFromName(name: String): String {
var section = name.first().uppercaseChar()
if (!section.isLetter()) section = '#'
return section.toString()
}
private fun playSongs(songs: List<MusicDirectory.Entry?>?) {
mediaPlayerController.addToPlaylist(
songs,
save = false,
autoPlay = true,
playNext = false,
shuffle = false,
newPlaylist = true
)
}
private fun playSong(song: MusicDirectory.Entry) {
mediaPlayerController.addToPlaylist(
listOf(song),
save = false,
autoPlay = false,
playNext = true,
shuffle = false,
newPlaylist = false
)
if (mediaPlayerController.playlistSize > 1) mediaPlayerController.next()
else mediaPlayerController.play()
}
private fun <T> callWithErrorHandling(function: () -> T): T? {
// TODO Implement better error handling
return try {
function()
} catch (all: Exception) {
Timber.i(all)
null
}
}
}