mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-06-18 16:13:02 +03:00
Implemented Media Browsing
This commit is contained in:
parent
f50d6f13f4
commit
cf05d3c781
@ -13,8 +13,11 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
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.MediaSessionEventDistributor
|
||||||
import org.moire.ultrasonic.util.MediaSessionEventListener
|
import org.moire.ultrasonic.util.MediaSessionEventListener
|
||||||
import org.moire.ultrasonic.util.MediaSessionHandler
|
import org.moire.ultrasonic.util.MediaSessionHandler
|
||||||
@ -23,6 +26,7 @@ import timber.log.Timber
|
|||||||
|
|
||||||
const val MEDIA_ROOT_ID = "MEDIA_ROOT_ID"
|
const val MEDIA_ROOT_ID = "MEDIA_ROOT_ID"
|
||||||
const val MEDIA_ALBUM_ID = "MEDIA_ALBUM_ID"
|
const val MEDIA_ALBUM_ID = "MEDIA_ALBUM_ID"
|
||||||
|
const val MEDIA_ALBUM_PAGE_ID = "MEDIA_ALBUM_PAGE_ID"
|
||||||
const val MEDIA_ALBUM_NEWEST_ID = "MEDIA_ALBUM_NEWEST_ID"
|
const val MEDIA_ALBUM_NEWEST_ID = "MEDIA_ALBUM_NEWEST_ID"
|
||||||
const val MEDIA_ALBUM_RECENT_ID = "MEDIA_ALBUM_RECENT_ID"
|
const val MEDIA_ALBUM_RECENT_ID = "MEDIA_ALBUM_RECENT_ID"
|
||||||
const val MEDIA_ALBUM_FREQUENT_ID = "MEDIA_ALBUM_FREQUENT_ID"
|
const val MEDIA_ALBUM_FREQUENT_ID = "MEDIA_ALBUM_FREQUENT_ID"
|
||||||
@ -38,12 +42,22 @@ const val MEDIA_BOOKMARK_ID = "MEDIA_BOOKMARK_ID"
|
|||||||
const val MEDIA_PODCAST_ID = "MEDIA_PODCAST_ID"
|
const val MEDIA_PODCAST_ID = "MEDIA_PODCAST_ID"
|
||||||
const val MEDIA_ALBUM_ITEM = "MEDIA_ALBUM_ITEM"
|
const val MEDIA_ALBUM_ITEM = "MEDIA_ALBUM_ITEM"
|
||||||
const val MEDIA_PLAYLIST_SONG_ITEM = "MEDIA_PLAYLIST_SONG_ITEM"
|
const val MEDIA_PLAYLIST_SONG_ITEM = "MEDIA_PLAYLIST_SONG_ITEM"
|
||||||
const val MEDIA_PLAYLIST_ITEM = "MEDIA_ALBUM_ITEM"
|
const val MEDIA_PLAYLIST_ITEM = "MEDIA_PLAYLIST_ITEM"
|
||||||
const val MEDIA_ARTIST_ITEM = "MEDIA_ARTIST_ITEM"
|
const val MEDIA_ARTIST_ITEM = "MEDIA_ARTIST_ITEM"
|
||||||
const val MEDIA_ARTIST_SECTION = "MEDIA_ARTIST_SECTION"
|
const val MEDIA_ARTIST_SECTION = "MEDIA_ARTIST_SECTION"
|
||||||
|
const val MEDIA_ALBUM_SONG_ITEM = "MEDIA_ALBUM_SONG_ITEM"
|
||||||
|
const val MEDIA_SONG_STARRED_ITEM = "MEDIA_SONG_STARRED_ITEM"
|
||||||
|
const val MEDIA_SONG_RANDOM_ITEM = "MEDIA_SONG_RANDOM_ITEM"
|
||||||
|
const val MEDIA_SHARE_ITEM = "MEDIA_SHARE_ITEM"
|
||||||
|
const val MEDIA_SHARE_SONG_ITEM = "MEDIA_SHARE_SONG_ITEM"
|
||||||
|
const val MEDIA_BOOKMARK_ITEM = "MEDIA_BOOKMARK_ITEM"
|
||||||
|
const val MEDIA_PODCAST_ITEM = "MEDIA_PODCAST_ITEM"
|
||||||
|
const val MEDIA_PODCAST_EPISODE_ITEM = "MEDIA_PODCAST_EPISODE_ITEM"
|
||||||
|
const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM"
|
||||||
|
|
||||||
// Currently the display limit for long lists is 100 items
|
// Currently the display limit for long lists is 100 items
|
||||||
const val displayLimit = 100
|
const val displayLimit = 100
|
||||||
|
const val searchLimit = 10
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MediaBrowserService implementation for e.g. Android Auto
|
* MediaBrowserService implementation for e.g. Android Auto
|
||||||
@ -56,12 +70,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
private val mediaSessionHandler by inject<MediaSessionHandler>()
|
private val mediaSessionHandler by inject<MediaSessionHandler>()
|
||||||
private val mediaPlayerController by inject<MediaPlayerController>()
|
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
private val musicService by lazy { MusicServiceFactory.getMusicService() }
|
private val musicService = MusicServiceFactory.getMusicService()
|
||||||
|
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||||
|
|
||||||
private var playlistCache: List<MusicDirectory.Entry>? = null
|
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 isOffline get() = ActiveServerProvider.isOffline()
|
||||||
private val useId3Tags get() = Util.getShouldUseId3Tags()
|
private val useId3Tags get() = Util.getShouldUseId3Tags()
|
||||||
@ -86,11 +103,42 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
when (mediaIdParts.first()) {
|
when (mediaIdParts.first()) {
|
||||||
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
|
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
|
||||||
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(mediaIdParts[1], mediaIdParts[2], mediaIdParts[3])
|
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?) {
|
override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {
|
||||||
// TODO implement
|
Timber.d("AutoMediaBrowserService onPlayFromSearchRequested query: %s", query)
|
||||||
|
if (query.isNullOrBlank()) playRandomSongs()
|
||||||
|
|
||||||
|
serviceScope.launch {
|
||||||
|
val criteria = SearchCriteria(query!!, 0, 0, displayLimit)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +179,8 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
extras.putInt(
|
extras.putInt(
|
||||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
|
||||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
|
||||||
|
extras.putBoolean(
|
||||||
|
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
|
||||||
|
|
||||||
return BrowserRoot(MEDIA_ROOT_ID, extras)
|
return BrowserRoot(MEDIA_ROOT_ID, extras)
|
||||||
}
|
}
|
||||||
@ -148,20 +198,24 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
MEDIA_LIBRARY_ID -> return getLibrary(result)
|
MEDIA_LIBRARY_ID -> return getLibrary(result)
|
||||||
MEDIA_ARTIST_ID -> return getArtists(result)
|
MEDIA_ARTIST_ID -> return getArtists(result)
|
||||||
MEDIA_ARTIST_SECTION -> return getArtists(result, parentIdParts[1])
|
MEDIA_ARTIST_SECTION -> return getArtists(result, parentIdParts[1])
|
||||||
MEDIA_ALBUM_ID -> return getAlbums(result)
|
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_PLAYLIST_ID -> return getPlaylists(result)
|
||||||
MEDIA_ALBUM_FREQUENT_ID -> return getFrequentAlbums(result)
|
MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(result, AlbumListType.FREQUENT)
|
||||||
MEDIA_ALBUM_NEWEST_ID -> return getNewestAlbums(result)
|
MEDIA_ALBUM_NEWEST_ID -> return getAlbums(result, AlbumListType.NEWEST)
|
||||||
MEDIA_ALBUM_RECENT_ID -> return getRecentAlbums(result)
|
MEDIA_ALBUM_RECENT_ID -> return getAlbums(result, AlbumListType.RECENT)
|
||||||
MEDIA_ALBUM_RANDOM_ID -> return getRandomAlbums(result)
|
MEDIA_ALBUM_RANDOM_ID -> return getAlbums(result, AlbumListType.RANDOM)
|
||||||
MEDIA_ALBUM_STARRED_ID -> return getStarredAlbums(result)
|
MEDIA_ALBUM_STARRED_ID -> return getAlbums(result, AlbumListType.STARRED)
|
||||||
MEDIA_SONG_RANDOM_ID -> return getRandomSongs(result)
|
MEDIA_SONG_RANDOM_ID -> return getRandomSongs(result)
|
||||||
MEDIA_SONG_STARRED_ID -> return getStarredSongs(result)
|
MEDIA_SONG_STARRED_ID -> return getStarredSongs(result)
|
||||||
MEDIA_SHARE_ID -> return getShares(result)
|
MEDIA_SHARE_ID -> return getShares(result)
|
||||||
MEDIA_BOOKMARK_ID -> return getBookmarks(result)
|
MEDIA_BOOKMARK_ID -> return getBookmarks(result)
|
||||||
MEDIA_PODCAST_ID -> return getPodcasts(result)
|
MEDIA_PODCAST_ID -> return getPodcasts(result)
|
||||||
MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2], result)
|
MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2], result)
|
||||||
MEDIA_ARTIST_ITEM -> return getAlbums(result, parentIdParts[1])
|
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())
|
else -> result.sendResult(mutableListOf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,19 +225,73 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
extras: Bundle?,
|
extras: Bundle?,
|
||||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
||||||
) {
|
) {
|
||||||
super.onSearch(query, extras, result)
|
Timber.d("AutoMediaBrowserService onSearch query: %s", query)
|
||||||
// TODO implement
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
|
||||||
|
serviceScope.launch {
|
||||||
|
val criteria = SearchCriteria(query, searchLimit, searchLimit, searchLimit)
|
||||||
|
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>>) {
|
private fun getRootItems(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
|
||||||
mediaItems.add(
|
if (!isOffline)
|
||||||
R.string.music_library_label,
|
mediaItems.add(
|
||||||
MEDIA_LIBRARY_ID,
|
R.string.music_library_label,
|
||||||
R.drawable.ic_library,
|
MEDIA_LIBRARY_ID,
|
||||||
null
|
R.drawable.ic_library,
|
||||||
)
|
null
|
||||||
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_artists_title,
|
R.string.main_artists_title,
|
||||||
@ -192,12 +300,13 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
if (!isOffline)
|
||||||
R.string.main_albums_title,
|
mediaItems.add(
|
||||||
MEDIA_ALBUM_ID,
|
R.string.main_albums_title,
|
||||||
R.drawable.ic_menu_browse_dark,
|
MEDIA_ALBUM_ID,
|
||||||
null
|
R.drawable.ic_menu_browse_dark,
|
||||||
)
|
null
|
||||||
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.playlist_label,
|
R.string.playlist_label,
|
||||||
@ -276,42 +385,125 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
result.detach()
|
result.detach()
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
|
val childMediaId: String
|
||||||
var artists = if (!isOffline && useId3Tags) {
|
var artists = if (!isOffline && useId3Tags) {
|
||||||
|
childMediaId = MEDIA_ARTIST_ITEM
|
||||||
// TODO this list can be big so we're not refreshing.
|
// TODO this list can be big so we're not refreshing.
|
||||||
// Maybe a refresh menu item can be added
|
// Maybe a refresh menu item can be added
|
||||||
musicService.getArtists(false)
|
callWithErrorHandling { musicService.getArtists(false) }
|
||||||
} else {
|
} else {
|
||||||
musicService.getIndexes(musicFolderId, false)
|
// This will be handled at getSongsForAlbum, which supports navigation
|
||||||
|
childMediaId = MEDIA_ALBUM_ITEM
|
||||||
|
callWithErrorHandling { musicService.getIndexes(musicFolderId, false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section != null)
|
if (artists != null) {
|
||||||
artists = artists.filter {
|
if (section != null)
|
||||||
artist -> getSectionFromName(artist.name ?: "") == section
|
artists = artists.filter { artist ->
|
||||||
}
|
getSectionFromName(artist.name ?: "") == section
|
||||||
|
}
|
||||||
|
|
||||||
// If there are too many artists, create alphabetic index of them
|
// If there are too many artists, create alphabetic index of them
|
||||||
if (section == null && artists.count() > displayLimit) {
|
if (section == null && artists.count() > displayLimit) {
|
||||||
val index = mutableListOf<String>()
|
val index = mutableListOf<String>()
|
||||||
// TODO This sort should use ignoredArticles somehow...
|
// TODO This sort should use ignoredArticles somehow...
|
||||||
artists = artists.sortedBy { artist -> artist.name }
|
artists = artists.sortedBy { artist -> artist.name }
|
||||||
artists.map { artist ->
|
artists.map { artist ->
|
||||||
val currentSection = getSectionFromName(artist.name ?: "")
|
val currentSection = getSectionFromName(artist.name ?: "")
|
||||||
if (!index.contains(currentSection)) {
|
if (!index.contains(currentSection)) {
|
||||||
index.add(currentSection)
|
index.add(currentSection)
|
||||||
|
mediaItems.add(
|
||||||
|
currentSection,
|
||||||
|
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
artists.map { artist ->
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
currentSection,
|
artist.name ?: "",
|
||||||
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"),
|
listOf(childMediaId, artist.id, artist.name).joinToString("|"),
|
||||||
null
|
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 {
|
} else {
|
||||||
artists.map { artist ->
|
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) }
|
||||||
mediaItems.add(
|
}
|
||||||
artist.name ?: "",
|
|
||||||
listOf(MEDIA_ARTIST_ITEM, artist.id).joinToString("|"),
|
albums?.getAllChild()?.map { album ->
|
||||||
null
|
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(displayLimit)
|
||||||
|
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)
|
result.sendResult(mediaItems)
|
||||||
@ -320,11 +512,38 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
private fun getAlbums(
|
private fun getAlbums(
|
||||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
|
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
|
||||||
artistId: String? = null
|
type: AlbumListType,
|
||||||
|
page: Int? = null
|
||||||
) {
|
) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
result.detach()
|
result.detach()
|
||||||
result.sendResult(mediaItems)
|
serviceScope.launch {
|
||||||
|
val offset = (page ?: 0) * displayLimit
|
||||||
|
val albums = if (useId3Tags) {
|
||||||
|
callWithErrorHandling { musicService.getAlbumList2(type.typeName, displayLimit, offset, null) }
|
||||||
|
} else {
|
||||||
|
callWithErrorHandling { musicService.getAlbumList(type.typeName, displayLimit, 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 >= displayLimit)
|
||||||
|
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>>) {
|
private fun getPlaylists(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
@ -332,8 +551,8 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
result.detach()
|
result.detach()
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
val playlists = musicService.getPlaylists(true)
|
val playlists = callWithErrorHandling { musicService.getPlaylists(true) }
|
||||||
playlists.map { playlist ->
|
playlists?.map { playlist ->
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
playlist.name,
|
playlist.name,
|
||||||
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
|
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
|
||||||
@ -350,28 +569,34 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
result.detach()
|
result.detach()
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
val content = musicService.getPlaylist(id, name)
|
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
|
||||||
|
|
||||||
mediaItems.add(
|
if (content != null) {
|
||||||
R.string.select_album_play_all,
|
if (content.getAllChild().count() > 1)
|
||||||
listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|"),
|
mediaItems.addPlayAllItem(
|
||||||
R.drawable.ic_stat_play_dark,
|
listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|")
|
||||||
null,
|
)
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
// Playlist should be cached as it may contain random elements
|
// Playlist should be cached as it may contain random elements
|
||||||
playlistCache = content.getAllChild()
|
playlistCache = content.getAllChild()
|
||||||
playlistCache!!.take(displayLimit).map { item ->
|
playlistCache!!.take(displayLimit).map { item ->
|
||||||
mediaItems.add(MediaBrowserCompat.MediaItem(
|
mediaItems.add(
|
||||||
Util.getMediaDescriptionForEntry(
|
MediaBrowserCompat.MediaItem(
|
||||||
item,
|
Util.getMediaDescriptionForEntry(
|
||||||
listOf(MEDIA_PLAYLIST_SONG_ITEM, id, name, item.id).joinToString("|")
|
item,
|
||||||
),
|
listOf(
|
||||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
MEDIA_PLAYLIST_SONG_ITEM,
|
||||||
))
|
id,
|
||||||
|
name,
|
||||||
|
item.id
|
||||||
|
).joinToString("|")
|
||||||
|
),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result.sendResult(mediaItems)
|
||||||
}
|
}
|
||||||
result.sendResult(mediaItems)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,17 +604,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
if (playlistCache == null) {
|
if (playlistCache == null) {
|
||||||
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
|
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
|
||||||
val content = musicService.getPlaylist(id, name)
|
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
|
||||||
playlistCache = content.getAllChild()
|
playlistCache = content?.getAllChild()
|
||||||
}
|
}
|
||||||
mediaPlayerController.download(
|
if (playlistCache != null) playSongs(playlistCache)
|
||||||
playlistCache,
|
|
||||||
save = false,
|
|
||||||
autoPlay = true,
|
|
||||||
playNext = false,
|
|
||||||
shuffle = false,
|
|
||||||
newPlaylist = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,88 +615,324 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
if (playlistCache == null) {
|
if (playlistCache == null) {
|
||||||
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
|
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
|
||||||
val content = musicService.getPlaylist(id, name)
|
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
|
||||||
playlistCache = content.getAllChild()
|
playlistCache = content?.getAllChild()
|
||||||
}
|
|
||||||
val song = playlistCache!!.firstOrNull{x -> x.id == songId}
|
|
||||||
if (song != null) {
|
|
||||||
mediaPlayerController.download(
|
|
||||||
listOf(song),
|
|
||||||
save = false,
|
|
||||||
autoPlay = false,
|
|
||||||
playNext = true,
|
|
||||||
shuffle = false,
|
|
||||||
newPlaylist = false
|
|
||||||
)
|
|
||||||
mediaPlayerController.next()
|
|
||||||
}
|
}
|
||||||
|
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>>) {
|
private fun getPodcasts(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
result.detach()
|
result.detach()
|
||||||
result.sendResult(mediaItems)
|
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>>) {
|
private fun getBookmarks(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
result.detach()
|
result.detach()
|
||||||
result.sendResult(mediaItems)
|
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>>) {
|
private fun getShares(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
result.detach()
|
result.detach()
|
||||||
result.sendResult(mediaItems)
|
|
||||||
|
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>>) {
|
private fun getStarredSongs(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
result.detach()
|
result.detach()
|
||||||
result.sendResult(mediaItems)
|
|
||||||
|
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(displayLimit)
|
||||||
|
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>>) {
|
private fun getRandomSongs(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
result.detach()
|
result.detach()
|
||||||
result.sendResult(mediaItems)
|
|
||||||
|
serviceScope.launch {
|
||||||
|
val songs = callWithErrorHandling { musicService.getRandomSongs(displayLimit) }
|
||||||
|
|
||||||
|
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 getStarredAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
private fun playRandomSongs() {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
serviceScope.launch {
|
||||||
result.detach()
|
if (randomSongsCache == null) {
|
||||||
result.sendResult(mediaItems)
|
// 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(displayLimit) }
|
||||||
|
randomSongsCache = content?.getAllChild()
|
||||||
|
}
|
||||||
|
if (randomSongsCache != null) playSongs(randomSongsCache)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRandomAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
private fun playRandomSong(songId: String) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
serviceScope.launch {
|
||||||
result.detach()
|
// If there is no cache, we can't play the selected song.
|
||||||
result.sendResult(mediaItems)
|
if (randomSongsCache != null) {
|
||||||
|
val song = randomSongsCache!!.firstOrNull { x -> x.id == songId }
|
||||||
|
if (song != null) playSong(song)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRecentAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
private fun listSongsInMusicService(id: String, name: String): MusicDirectory? {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
return if (!ActiveServerProvider.isOffline() && Util.getShouldUseId3Tags()) {
|
||||||
result.detach()
|
callWithErrorHandling { musicService.getAlbum(id, name, false) }
|
||||||
result.sendResult(mediaItems)
|
} else {
|
||||||
|
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNewestAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
private fun listStarredSongsInMusicService(): SearchResult? {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
return if (Util.getShouldUseId3Tags()) {
|
||||||
result.detach()
|
callWithErrorHandling { musicService.getStarred2() }
|
||||||
result.sendResult(mediaItems)
|
} else {
|
||||||
}
|
callWithErrorHandling { musicService.getStarred() }
|
||||||
|
}
|
||||||
private fun getFrequentAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
|
||||||
result.detach()
|
|
||||||
result.sendResult(mediaItems)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<MediaBrowserCompat.MediaItem>.add(
|
private fun MutableList<MediaBrowserCompat.MediaItem>.add(
|
||||||
title: String,
|
title: String,
|
||||||
mediaId: String,
|
mediaId: String,
|
||||||
icon: Int?,
|
icon: Int?,
|
||||||
|
groupNameId: Int? = null
|
||||||
) {
|
) {
|
||||||
val builder = MediaDescriptionCompat.Builder()
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
builder.setTitle(title)
|
builder.setTitle(title)
|
||||||
@ -487,6 +941,12 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
if (icon != null)
|
if (icon != null)
|
||||||
builder.setIconUri(Util.getUriToDrawable(applicationContext, icon))
|
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(
|
val mediaItem = MediaBrowserCompat.MediaItem(
|
||||||
builder.build(),
|
builder.build(),
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
@ -524,9 +984,54 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
this.add(mediaItem)
|
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 {
|
private fun getSectionFromName(name: String): String {
|
||||||
var section = name.first().uppercaseChar()
|
var section = name.first().uppercaseChar()
|
||||||
if (!section.isLetter()) section = '#'
|
if (!section.isLetter()) section = '#'
|
||||||
return section.toString()
|
return section.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun playSongs(songs: List<MusicDirectory.Entry?>?) {
|
||||||
|
mediaPlayerController.download(
|
||||||
|
songs,
|
||||||
|
save = false,
|
||||||
|
autoPlay = true,
|
||||||
|
playNext = false,
|
||||||
|
shuffle = false,
|
||||||
|
newPlaylist = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playSong(song: MusicDirectory.Entry) {
|
||||||
|
mediaPlayerController.download(
|
||||||
|
listOf(song),
|
||||||
|
save = false,
|
||||||
|
autoPlay = false,
|
||||||
|
playNext = true,
|
||||||
|
shuffle = false,
|
||||||
|
newPlaylist = false
|
||||||
|
)
|
||||||
|
mediaPlayerController.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> callWithErrorHandling(function: () -> T): T? {
|
||||||
|
// TODO Implement better error handling
|
||||||
|
return try {
|
||||||
|
function()
|
||||||
|
} catch (all: Exception) {
|
||||||
|
Timber.i(all)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,344 +0,0 @@
|
|||||||
package org.moire.ultrasonic.service
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
|
||||||
import androidx.media.utils.MediaConstants
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
|
||||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
|
||||||
import org.moire.ultrasonic.domain.PlayerState
|
|
||||||
import org.moire.ultrasonic.fragment.AlbumListModel
|
|
||||||
import org.moire.ultrasonic.fragment.ArtistListModel
|
|
||||||
import org.moire.ultrasonic.util.Constants
|
|
||||||
import org.moire.ultrasonic.util.Pair
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.ObjectInputStream
|
|
||||||
import java.io.ObjectOutputStream
|
|
||||||
import java.util.concurrent.ExecutorService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
class AutoMediaPlayerService: MediaBrowserServiceCompat() {
|
|
||||||
|
|
||||||
val mediaPlayerService : MediaPlayerService = MediaPlayerService()
|
|
||||||
var albumListModel: AlbumListModel? = null
|
|
||||||
var artistListModel: ArtistListModel? = null
|
|
||||||
|
|
||||||
val executorService: ExecutorService = Executors.newFixedThreadPool(4)
|
|
||||||
var maximumRootChildLimit: Int = 4
|
|
||||||
|
|
||||||
private val MEDIA_BROWSER_ROOT_ID = "_Ultrasonice_mb_root_"
|
|
||||||
|
|
||||||
private val MEDIA_BROWSER_RECENT_LIST_ROOT = "_Ultrasonic_mb_recent_list_root_"
|
|
||||||
private val MEDIA_BROWSER_ALBUM_LIST_ROOT = "_Ultrasonic_mb_album_list_root_"
|
|
||||||
private val MEDIA_BROWSER_ARTIST_LIST_ROOT = "_Ultrasonic_mb_rtist_list_root_"
|
|
||||||
|
|
||||||
private val MEDIA_BROWSER_RECENT_PREFIX = "_Ultrasonic_mb_recent_prefix_"
|
|
||||||
private val MEDIA_BROWSER_ALBUM_PREFIX = "_Ultrasonic_mb_album_prefix_"
|
|
||||||
private val MEDIA_BROWSER_ARTIST_PREFIX = "_Ultrasonic_mb_artist_prefix_"
|
|
||||||
|
|
||||||
private val MEDIA_BROWSER_EXTRA_ALBUM_LIST = "_Ultrasonic_mb_extra_album_list_"
|
|
||||||
private val MEDIA_BROWSER_EXTRA_MEDIA_ID = "_Ultrasonic_mb_extra_media_id_"
|
|
||||||
|
|
||||||
class AlbumListObserver(
|
|
||||||
val idPrefix: String,
|
|
||||||
val result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>,
|
|
||||||
data: LiveData<List<MusicDirectory.Entry>>
|
|
||||||
) :
|
|
||||||
Observer<List<MusicDirectory.Entry>> {
|
|
||||||
|
|
||||||
private var liveData: LiveData<List<MusicDirectory.Entry>>? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Order is very important here. When observerForever is called onChanged
|
|
||||||
// will immediately be called with any past data updates. We don't care
|
|
||||||
// about those. So by having it called *before* liveData is set will
|
|
||||||
// signal to onChanged to ignore the first input
|
|
||||||
data.observeForever(this)
|
|
||||||
liveData = data
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChanged(albumList: List<MusicDirectory.Entry>?) {
|
|
||||||
if (liveData == null) {
|
|
||||||
// See comment in the initializer
|
|
||||||
return
|
|
||||||
}
|
|
||||||
liveData!!.removeObserver(this)
|
|
||||||
if (albumList == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
|
||||||
for (item in albumList) {
|
|
||||||
val entryBuilder: MediaDescriptionCompat.Builder =
|
|
||||||
MediaDescriptionCompat.Builder()
|
|
||||||
entryBuilder
|
|
||||||
.setTitle(item.title)
|
|
||||||
.setMediaId(idPrefix + item.id)
|
|
||||||
mediaItems.add(
|
|
||||||
MediaBrowserCompat.MediaItem(
|
|
||||||
entryBuilder.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.sendResult(mediaItems)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArtistListObserver(
|
|
||||||
val idPrefix: String,
|
|
||||||
val result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>,
|
|
||||||
data: LiveData<List<ArtistOrIndex>>
|
|
||||||
) :
|
|
||||||
Observer<List<ArtistOrIndex>> {
|
|
||||||
|
|
||||||
private var liveData: LiveData<List<ArtistOrIndex>>? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Order is very important here. When observerForever is called onChanged
|
|
||||||
// will immediately be called with any past data updates. We don't care
|
|
||||||
// about those. So by having it called *before* liveData is set will
|
|
||||||
// signal to onChanged to ignore the first input
|
|
||||||
data.observeForever(this)
|
|
||||||
liveData = data
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChanged(artistList: List<ArtistOrIndex>?) {
|
|
||||||
if (liveData == null) {
|
|
||||||
// See comment in the initializer
|
|
||||||
return
|
|
||||||
}
|
|
||||||
liveData!!.removeObserver(this)
|
|
||||||
if (artistList == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
|
||||||
for (item in artistList) {
|
|
||||||
val entryBuilder: MediaDescriptionCompat.Builder =
|
|
||||||
MediaDescriptionCompat.Builder()
|
|
||||||
entryBuilder
|
|
||||||
.setTitle(item.name)
|
|
||||||
.setMediaId(idPrefix + item.id)
|
|
||||||
mediaItems.add(
|
|
||||||
MediaBrowserCompat.MediaItem(
|
|
||||||
entryBuilder.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.sendResult(mediaItems)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
albumListModel = AlbumListModel(application)
|
|
||||||
artistListModel = ArtistListModel(application)
|
|
||||||
|
|
||||||
//mediaPlayerService.onCreate()
|
|
||||||
//mediaPlayerService.updateMediaSession(null, PlayerState.IDLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
|
|
||||||
if (rootHints != null) {
|
|
||||||
maximumRootChildLimit = rootHints.getInt(
|
|
||||||
MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
|
|
||||||
4
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// opt into the root tabs (because it's gonna be non-optional
|
|
||||||
// real soon anyway)
|
|
||||||
val extras = Bundle()
|
|
||||||
val TABS_OPT_IN_HINT = "android.media.browse.AUTO_TABS_OPT_IN_HINT"
|
|
||||||
extras.putBoolean(TABS_OPT_IN_HINT, true)
|
|
||||||
return MediaBrowserServiceCompat.BrowserRoot(MEDIA_BROWSER_ROOT_ID, extras)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadChildren(parentId: String, result: Result<List<MediaBrowserCompat.MediaItem>>) {
|
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
|
||||||
|
|
||||||
if (MEDIA_BROWSER_ROOT_ID == parentId) {
|
|
||||||
// Build the MediaItem objects for the top level,
|
|
||||||
// and put them in the mediaItems list...
|
|
||||||
|
|
||||||
var recentList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder()
|
|
||||||
recentList.setTitle("Recent").setMediaId(MEDIA_BROWSER_RECENT_LIST_ROOT)
|
|
||||||
mediaItems.add(
|
|
||||||
MediaBrowserCompat.MediaItem(
|
|
||||||
recentList.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
var albumList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder()
|
|
||||||
albumList.setTitle("Albums").setMediaId(MEDIA_BROWSER_ALBUM_LIST_ROOT)
|
|
||||||
mediaItems.add(
|
|
||||||
MediaBrowserCompat.MediaItem(
|
|
||||||
albumList.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
var artistList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder()
|
|
||||||
artistList.setTitle("Artists").setMediaId(MEDIA_BROWSER_ARTIST_LIST_ROOT)
|
|
||||||
mediaItems.add(
|
|
||||||
MediaBrowserCompat.MediaItem(
|
|
||||||
artistList.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else if (MEDIA_BROWSER_RECENT_LIST_ROOT == parentId) {
|
|
||||||
fetchAlbumList(AlbumListType.RECENT, MEDIA_BROWSER_RECENT_PREFIX, result)
|
|
||||||
return
|
|
||||||
} else if (MEDIA_BROWSER_ALBUM_LIST_ROOT == parentId) {
|
|
||||||
fetchAlbumList(AlbumListType.SORTED_BY_NAME, MEDIA_BROWSER_ALBUM_PREFIX, result)
|
|
||||||
return
|
|
||||||
} else if (MEDIA_BROWSER_ARTIST_LIST_ROOT == parentId) {
|
|
||||||
fetchArtistList(MEDIA_BROWSER_ARTIST_PREFIX, result)
|
|
||||||
return
|
|
||||||
} else if (parentId.startsWith(MEDIA_BROWSER_RECENT_PREFIX)) {
|
|
||||||
fetchTrackList(parentId.substring(MEDIA_BROWSER_RECENT_PREFIX.length), result)
|
|
||||||
return
|
|
||||||
} else if (parentId.startsWith(MEDIA_BROWSER_ALBUM_PREFIX)) {
|
|
||||||
fetchTrackList(parentId.substring(MEDIA_BROWSER_ALBUM_PREFIX.length), result)
|
|
||||||
return
|
|
||||||
} else if (parentId.startsWith(MEDIA_BROWSER_ARTIST_PREFIX)) {
|
|
||||||
fetchArtistAlbumList(
|
|
||||||
parentId.substring(MEDIA_BROWSER_ARTIST_PREFIX.length),
|
|
||||||
result
|
|
||||||
)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// Examine the passed parentMediaId to see which submenu we're at,
|
|
||||||
// and put the children of that menu in the mediaItems list...
|
|
||||||
}
|
|
||||||
result.sendResult(mediaItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun getBundleData(bundle: Bundle?): Pair<String, List<MusicDirectory.Entry>>? {
|
|
||||||
if (bundle == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bundle.containsKey(MEDIA_BROWSER_EXTRA_ALBUM_LIST) ||
|
|
||||||
!bundle.containsKey(MEDIA_BROWSER_EXTRA_MEDIA_ID)
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val bytes = bundle.getByteArray(MEDIA_BROWSER_EXTRA_ALBUM_LIST)
|
|
||||||
val byteArrayInputStream = ByteArrayInputStream(bytes)
|
|
||||||
val objectInputStream = ObjectInputStream(byteArrayInputStream)
|
|
||||||
return Pair(
|
|
||||||
bundle.getString(MEDIA_BROWSER_EXTRA_MEDIA_ID),
|
|
||||||
objectInputStream.readObject() as List<MusicDirectory.Entry>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchAlbumList(
|
|
||||||
type: AlbumListType,
|
|
||||||
idPrefix: String,
|
|
||||||
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
|
|
||||||
) {
|
|
||||||
AutoMediaPlayerService.AlbumListObserver(
|
|
||||||
idPrefix, result,
|
|
||||||
albumListModel!!.albumList
|
|
||||||
)
|
|
||||||
|
|
||||||
val args: Bundle = Bundle()
|
|
||||||
args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type.toString())
|
|
||||||
albumListModel!!.getAlbumList(false, null, args)
|
|
||||||
result.detach()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchArtistList(
|
|
||||||
idPrefix: String,
|
|
||||||
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
|
|
||||||
) {
|
|
||||||
AutoMediaPlayerService.ArtistListObserver(idPrefix, result, artistListModel!!.artists)
|
|
||||||
|
|
||||||
artistListModel!!.getItems(false, null)
|
|
||||||
result.detach()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchArtistAlbumList(
|
|
||||||
id: String,
|
|
||||||
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
|
|
||||||
) {
|
|
||||||
executorService.execute {
|
|
||||||
val musicService = MusicServiceFactory.getMusicService()
|
|
||||||
|
|
||||||
val musicDirectory = musicService.getMusicDirectory(
|
|
||||||
id, "", false
|
|
||||||
)
|
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
|
||||||
|
|
||||||
for (item in musicDirectory.getAllChild()) {
|
|
||||||
val entryBuilder: MediaDescriptionCompat.Builder =
|
|
||||||
MediaDescriptionCompat.Builder()
|
|
||||||
entryBuilder.setTitle(item.title).setMediaId(MEDIA_BROWSER_ALBUM_PREFIX + item.id)
|
|
||||||
mediaItems.add(
|
|
||||||
MediaBrowserCompat.MediaItem(
|
|
||||||
entryBuilder.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
result.sendResult(mediaItems)
|
|
||||||
}
|
|
||||||
result.detach()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchTrackList(
|
|
||||||
id: String,
|
|
||||||
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
|
|
||||||
) {
|
|
||||||
executorService.execute {
|
|
||||||
val musicService = MusicServiceFactory.getMusicService()
|
|
||||||
|
|
||||||
val albumDirectory = musicService.getAlbum(
|
|
||||||
id, "", false
|
|
||||||
)
|
|
||||||
|
|
||||||
// The idea here is that we want to attach the full album list to every song,
|
|
||||||
// as well as the id of the specific song. This way if someone chooses to play a song
|
|
||||||
// we can add the song and all subsequent songs in the album
|
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
|
||||||
val objectOutputStream = ObjectOutputStream(byteArrayOutputStream)
|
|
||||||
objectOutputStream.writeObject(albumDirectory.getAllChild())
|
|
||||||
objectOutputStream.close()
|
|
||||||
val songList = byteArrayOutputStream.toByteArray()
|
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
|
||||||
|
|
||||||
for (item in albumDirectory.getAllChild()) {
|
|
||||||
val extras = Bundle()
|
|
||||||
|
|
||||||
extras.putByteArray(
|
|
||||||
MEDIA_BROWSER_EXTRA_ALBUM_LIST,
|
|
||||||
songList
|
|
||||||
)
|
|
||||||
extras.putString(
|
|
||||||
MEDIA_BROWSER_EXTRA_MEDIA_ID,
|
|
||||||
item.id
|
|
||||||
)
|
|
||||||
|
|
||||||
val entryBuilder: MediaDescriptionCompat.Builder =
|
|
||||||
MediaDescriptionCompat.Builder()
|
|
||||||
entryBuilder.setTitle(item.title).setMediaId(item.id).setExtras(extras)
|
|
||||||
mediaItems.add(
|
|
||||||
MediaBrowserCompat.MediaItem(
|
|
||||||
entryBuilder.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
result.sendResult(mediaItems)
|
|
||||||
}
|
|
||||||
result.detach()
|
|
||||||
}
|
|
||||||
}
|
|
@ -535,7 +535,7 @@ class MediaPlayerService : Service() {
|
|||||||
// Init
|
// Init
|
||||||
val context = applicationContext
|
val context = applicationContext
|
||||||
val song = currentPlaying?.song
|
val song = currentPlaying?.song
|
||||||
val stopIntent = getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_STOP, 100)
|
val stopIntent = Util.getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_STOP, 100)
|
||||||
|
|
||||||
// We should use a single notification builder, otherwise the notification may not be updated
|
// We should use a single notification builder, otherwise the notification may not be updated
|
||||||
if (notificationBuilder == null) {
|
if (notificationBuilder == null) {
|
||||||
@ -654,7 +654,7 @@ class MediaPlayerService : Service() {
|
|||||||
else -> return null
|
else -> return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode)
|
val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode)
|
||||||
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
|
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -665,7 +665,7 @@ class MediaPlayerService : Service() {
|
|||||||
): NotificationCompat.Action {
|
): NotificationCompat.Action {
|
||||||
val isPlaying = playerState === PlayerState.STARTED
|
val isPlaying = playerState === PlayerState.STARTED
|
||||||
val keycode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
val keycode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||||
val pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode)
|
val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode)
|
||||||
val label: String
|
val label: String
|
||||||
val icon: Int
|
val icon: Int
|
||||||
|
|
||||||
@ -698,7 +698,7 @@ class MediaPlayerService : Service() {
|
|||||||
icon = R.drawable.ic_star_hollow_dark
|
icon = R.drawable.ic_star_hollow_dark
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingIntent = getPendingIntentForMediaAction(context, keyCode, requestCode)
|
val pendingIntent = Util.getPendingIntentForMediaAction(context, keyCode, requestCode)
|
||||||
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
|
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -710,18 +710,6 @@ class MediaPlayerService : Service() {
|
|||||||
return PendingIntent.getActivity(this, 0, intent, flags)
|
return PendingIntent.getActivity(this, 0, intent, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPendingIntentForMediaAction(
|
|
||||||
context: Context,
|
|
||||||
keycode: Int,
|
|
||||||
requestCode: Int
|
|
||||||
): PendingIntent {
|
|
||||||
val intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
|
||||||
val flags = PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
intent.setPackage(context.packageName)
|
|
||||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
|
|
||||||
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
companion object {
|
companion object {
|
||||||
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"
|
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"
|
||||||
|
@ -5,12 +5,10 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN
|
import android.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
@ -75,7 +73,7 @@ class MediaSessionHandler : KoinComponent {
|
|||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
super.onPlay()
|
super.onPlay()
|
||||||
|
|
||||||
getPendingIntentForMediaAction(
|
Util.getPendingIntentForMediaAction(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY,
|
KeyEvent.KEYCODE_MEDIA_PLAY,
|
||||||
keycode
|
keycode
|
||||||
@ -100,7 +98,7 @@ class MediaSessionHandler : KoinComponent {
|
|||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
getPendingIntentForMediaAction(
|
Util.getPendingIntentForMediaAction(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE,
|
KeyEvent.KEYCODE_MEDIA_PAUSE,
|
||||||
keycode
|
keycode
|
||||||
@ -110,7 +108,7 @@ class MediaSessionHandler : KoinComponent {
|
|||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
getPendingIntentForMediaAction(
|
Util.getPendingIntentForMediaAction(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP,
|
KeyEvent.KEYCODE_MEDIA_STOP,
|
||||||
keycode
|
keycode
|
||||||
@ -120,7 +118,7 @@ class MediaSessionHandler : KoinComponent {
|
|||||||
|
|
||||||
override fun onSkipToNext() {
|
override fun onSkipToNext() {
|
||||||
super.onSkipToNext()
|
super.onSkipToNext()
|
||||||
getPendingIntentForMediaAction(
|
Util.getPendingIntentForMediaAction(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT,
|
KeyEvent.KEYCODE_MEDIA_NEXT,
|
||||||
keycode
|
keycode
|
||||||
@ -130,7 +128,7 @@ class MediaSessionHandler : KoinComponent {
|
|||||||
|
|
||||||
override fun onSkipToPrevious() {
|
override fun onSkipToPrevious() {
|
||||||
super.onSkipToPrevious()
|
super.onSkipToPrevious()
|
||||||
getPendingIntentForMediaAction(
|
Util.getPendingIntentForMediaAction(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS,
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS,
|
||||||
keycode
|
keycode
|
||||||
@ -248,7 +246,6 @@ class MediaSessionHandler : KoinComponent {
|
|||||||
cachedPlaylist = playlist
|
cachedPlaylist = playlist
|
||||||
if (mediaSession == null) return
|
if (mediaSession == null) return
|
||||||
|
|
||||||
// TODO Implement Now Playing queue handling properly
|
|
||||||
mediaSession!!.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing))
|
mediaSession!!.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing))
|
||||||
mediaSession!!.setQueue(playlist.mapIndexed { id, song ->
|
mediaSession!!.setQueue(playlist.mapIndexed { id, song ->
|
||||||
MediaSessionCompat.QueueItem(
|
MediaSessionCompat.QueueItem(
|
||||||
@ -306,17 +303,4 @@ class MediaSessionHandler : KoinComponent {
|
|||||||
private fun unregisterMediaButtonEventReceiver() {
|
private fun unregisterMediaButtonEventReceiver() {
|
||||||
mediaSession?.setMediaButtonReceiver(null)
|
mediaSession?.setMediaButtonReceiver(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Copied from MediaPlayerService. Move to Utils
|
|
||||||
private fun getPendingIntentForMediaAction(
|
|
||||||
context: Context,
|
|
||||||
keycode: Int,
|
|
||||||
requestCode: Int
|
|
||||||
): PendingIntent {
|
|
||||||
val intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
|
||||||
val flags = PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
intent.setPackage(context.packageName)
|
|
||||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
|
|
||||||
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -21,6 +21,7 @@ package org.moire.ultrasonic.util
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
@ -37,15 +38,18 @@ import android.net.Uri
|
|||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.net.wifi.WifiManager.WifiLock
|
import android.net.wifi.WifiManager.WifiLock
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.AnyRes
|
import androidx.annotation.AnyRes
|
||||||
|
import androidx.media.utils.MediaConstants
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||||
@ -641,12 +645,13 @@ object Util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark>): MusicDirectory {
|
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark?>): MusicDirectory {
|
||||||
val musicDirectory = MusicDirectory()
|
val musicDirectory = MusicDirectory()
|
||||||
var song: MusicDirectory.Entry
|
var song: MusicDirectory.Entry
|
||||||
for ((position, _, _, _, _, entry) in bookmarks) {
|
for (bookmark in bookmarks) {
|
||||||
song = entry
|
if (bookmark == null) continue
|
||||||
song.bookmarkPosition = position
|
song = bookmark.entry
|
||||||
|
song.bookmarkPosition = bookmark.position
|
||||||
musicDirectory.addChild(song)
|
musicDirectory.addChild(song)
|
||||||
}
|
}
|
||||||
return musicDirectory
|
return musicDirectory
|
||||||
@ -1255,7 +1260,11 @@ object Util {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMediaDescriptionForEntry(song: MusicDirectory.Entry, mediaId: String? = null): MediaDescriptionCompat {
|
fun getMediaDescriptionForEntry(
|
||||||
|
song: MusicDirectory.Entry,
|
||||||
|
mediaId: String? = null,
|
||||||
|
groupNameId: Int? = null
|
||||||
|
): MediaDescriptionCompat {
|
||||||
|
|
||||||
val descriptionBuilder = MediaDescriptionCompat.Builder()
|
val descriptionBuilder = MediaDescriptionCompat.Builder()
|
||||||
val artist = StringBuilder(60)
|
val artist = StringBuilder(60)
|
||||||
@ -1266,7 +1275,7 @@ object Util {
|
|||||||
artist.append(String.format("%s ", formatTotalDuration(duration.toLong())))
|
artist.append(String.format("%s ", formatTotalDuration(duration.toLong())))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (song.bitRate != null)
|
if (song.bitRate != null && song.bitRate!! > 0)
|
||||||
bitRate = String.format(
|
bitRate = String.format(
|
||||||
appContext().getString(R.string.song_details_kbps), song.bitRate
|
appContext().getString(R.string.song_details_kbps), song.bitRate
|
||||||
)
|
)
|
||||||
@ -1282,7 +1291,7 @@ object Util {
|
|||||||
val artistName = song.artist
|
val artistName = song.artist
|
||||||
|
|
||||||
if (artistName != null) {
|
if (artistName != null) {
|
||||||
if (shouldDisplayBitrateWithArtist()) {
|
if (shouldDisplayBitrateWithArtist() && (!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank())) {
|
||||||
artist.append(artistName).append(" (").append(
|
artist.append(artistName).append(" (").append(
|
||||||
String.format(
|
String.format(
|
||||||
appContext().getString(R.string.song_details_all),
|
appContext().getString(R.string.song_details_all),
|
||||||
@ -1311,10 +1320,28 @@ object Util {
|
|||||||
).append(')')
|
).append(')')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (groupNameId != null)
|
||||||
|
descriptionBuilder.setExtras(Bundle().apply { putString(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||||
|
appContext().getString(groupNameId)
|
||||||
|
) })
|
||||||
|
|
||||||
descriptionBuilder.setTitle(title)
|
descriptionBuilder.setTitle(title)
|
||||||
descriptionBuilder.setSubtitle(artist)
|
descriptionBuilder.setSubtitle(artist)
|
||||||
descriptionBuilder.setMediaId(mediaId)
|
descriptionBuilder.setMediaId(mediaId)
|
||||||
|
|
||||||
return descriptionBuilder.build()
|
return descriptionBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPendingIntentForMediaAction(
|
||||||
|
context: Context,
|
||||||
|
keycode: Int,
|
||||||
|
requestCode: Int
|
||||||
|
): PendingIntent {
|
||||||
|
val intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||||
|
val flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
intent.setPackage(context.packageName)
|
||||||
|
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
|
||||||
|
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user