/* * 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 androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants import io.reactivex.rxjava3.disposables.CompositeDisposable 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.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 val lifecycleSupport by inject() private val mediaSessionHandler by inject() private val mediaPlayerController by inject() 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? = null private var starredSongsCache: List? = null private var randomSongsCache: List? = null private var searchSongsCache: List? = null private val isOffline get() = ActiveServerProvider.isOffline() private val useId3Tags get() = Settings.shouldUseId3Tags private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId private var rxBusSubscription: CompositeDisposable = CompositeDisposable() @Suppress("MagicNumber") override fun onCreate() { super.onCreate() rxBusSubscription += RxBus.mediaSessionTokenObservable.subscribe { if (sessionToken == null) sessionToken = it } rxBusSubscription += RxBus.playFromMediaIdCommandObservable.subscribe { playFromMediaId(it.first) } rxBusSubscription += RxBus.playFromSearchCommandObservable.subscribe { playFromSearchCommand(it.first) } 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") } @Suppress("MagicNumber", "ComplexMethod") private fun playFromMediaId(mediaId: String?) { 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]) } } private fun playFromSearchCommand(query: String?) { 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) } } } override fun onDestroy() { super.onDestroy() rxBusSubscription.dispose() 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> ) { 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> ) { Timber.d("AutoMediaBrowserService onSearch query: %s", query) val mediaItems: MutableList = 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>) { val mediaItems: MutableList = 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>) { val mediaItems: MutableList = 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>, section: String? = null ) { val mediaItems: MutableList = 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() // 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>, id: String, name: String ) { val mediaItems: MutableList = 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>, id: String, name: String ) { val mediaItems: MutableList = 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>, type: AlbumListType, page: Int? = null ) { val mediaItems: MutableList = 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>) { val mediaItems: MutableList = 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> ) { val mediaItems: MutableList = 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>) { val mediaItems: MutableList = 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>, id: String ) { val mediaItems: MutableList = 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>) { val mediaItems: MutableList = 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>) { val mediaItems: MutableList = 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>, id: String ) { val mediaItems: MutableList = 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>) { val mediaItems: MutableList = 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>) { val mediaItems: MutableList = 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.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.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.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?) { 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 callWithErrorHandling(function: () -> T): T? { // TODO Implement better error handling return try { function() } catch (all: Exception) { Timber.i(all) null } } }