Implemented Media Browsing

This commit is contained in:
Nite 2021-07-18 11:33:39 +02:00
parent f50d6f13f4
commit cf05d3c781
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
5 changed files with 669 additions and 509 deletions

View File

@ -13,8 +13,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.util.MediaSessionEventDistributor
import org.moire.ultrasonic.util.MediaSessionEventListener
import org.moire.ultrasonic.util.MediaSessionHandler
@ -23,6 +26,7 @@ import timber.log.Timber
const val MEDIA_ROOT_ID = "MEDIA_ROOT_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_RECENT_ID = "MEDIA_ALBUM_RECENT_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_ALBUM_ITEM = "MEDIA_ALBUM_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_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
const val displayLimit = 100
const val searchLimit = 10
/**
* MediaBrowserService implementation for e.g. Android Auto
@ -56,12 +70,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private val mediaSessionHandler by inject<MediaSessionHandler>()
private val mediaPlayerController by inject<MediaPlayerController>()
private val activeServerProvider: ActiveServerProvider by inject()
private val musicService by lazy { MusicServiceFactory.getMusicService() }
private val musicService = MusicServiceFactory.getMusicService()
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private var playlistCache: List<MusicDirectory.Entry>? = null
private var starredSongsCache: List<MusicDirectory.Entry>? = null
private var randomSongsCache: List<MusicDirectory.Entry>? = null
private var searchSongsCache: List<MusicDirectory.Entry>? = null
private val isOffline get() = ActiveServerProvider.isOffline()
private val useId3Tags get() = Util.getShouldUseId3Tags()
@ -86,11 +103,42 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
when (mediaIdParts.first()) {
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(mediaIdParts[1], mediaIdParts[2], mediaIdParts[3])
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(mediaIdParts[1], mediaIdParts[2], mediaIdParts[3])
MEDIA_SONG_STARRED_ID -> playStarredSongs()
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
MEDIA_SONG_RANDOM_ITEM -> playRandomSong(mediaIdParts[1])
MEDIA_SHARE_ITEM -> playShare(mediaIdParts[1])
MEDIA_SHARE_SONG_ITEM -> playShareSong(mediaIdParts[1], mediaIdParts[2])
MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1])
MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1])
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(mediaIdParts[1], mediaIdParts[2])
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
}
}
override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {
// 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(
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)
}
@ -148,20 +198,24 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
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)
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 getFrequentAlbums(result)
MEDIA_ALBUM_NEWEST_ID -> return getNewestAlbums(result)
MEDIA_ALBUM_RECENT_ID -> return getRecentAlbums(result)
MEDIA_ALBUM_RANDOM_ID -> return getRandomAlbums(result)
MEDIA_ALBUM_STARRED_ID -> return getStarredAlbums(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 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())
}
}
@ -171,19 +225,73 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
extras: Bundle?,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
super.onSearch(query, extras, result)
// TODO implement
Timber.d("AutoMediaBrowserService onSearch query: %s", query)
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>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
mediaItems.add(
R.string.music_library_label,
MEDIA_LIBRARY_ID,
R.drawable.ic_library,
null
)
if (!isOffline)
mediaItems.add(
R.string.music_library_label,
MEDIA_LIBRARY_ID,
R.drawable.ic_library,
null
)
mediaItems.add(
R.string.main_artists_title,
@ -192,12 +300,13 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
null
)
mediaItems.add(
R.string.main_albums_title,
MEDIA_ALBUM_ID,
R.drawable.ic_menu_browse_dark,
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,
@ -276,42 +385,125 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
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
musicService.getArtists(false)
callWithErrorHandling { musicService.getArtists(false) }
} 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)
artists = artists.filter {
artist -> getSectionFromName(artist.name ?: "") == section
}
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() > displayLimit) {
val index = mutableListOf<String>()
// TODO This sort should use ignoredArticles somehow...
artists = artists.sortedBy { artist -> artist.name }
artists.map { artist ->
val currentSection = getSectionFromName(artist.name ?: "")
if (!index.contains(currentSection)) {
index.add(currentSection)
// If there are too many artists, create alphabetic index of them
if (section == null && artists.count() > displayLimit) {
val index = mutableListOf<String>()
// TODO This sort should use ignoredArticles somehow...
artists = artists.sortedBy { artist -> artist.name }
artists.map { artist ->
val currentSection = getSectionFromName(artist.name ?: "")
if (!index.contains(currentSection)) {
index.add(currentSection)
mediaItems.add(
currentSection,
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"),
null
)
}
}
} else {
artists.map { artist ->
mediaItems.add(
currentSection,
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"),
artist.name ?: "",
listOf(childMediaId, artist.id, artist.name).joinToString("|"),
null
)
}
}
result.sendResult(mediaItems)
}
}
}
private fun getAlbumsForArtist(
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
id: String,
name: String
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val albums = if (!isOffline && useId3Tags) {
callWithErrorHandling { musicService.getArtist(id, name,false) }
} else {
artists.map { artist ->
mediaItems.add(
artist.name ?: "",
listOf(MEDIA_ARTIST_ITEM, artist.id).joinToString("|"),
null
)
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) }
}
albums?.getAllChild()?.map { album ->
mediaItems.add(
album.title ?: "",
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
.joinToString("|"),
null
)
}
result.sendResult(mediaItems)
}
}
private fun getSongsForAlbum(
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
id: String,
name: String
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
serviceScope.launch {
val songs = listSongsInMusicService(id, name)
if (songs != null) {
if (songs.getChildren(includeDirs = true, includeFiles = false).count() == 0 &&
songs.getChildren(includeDirs = false, includeFiles = true).count() > 0
)
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all?
val items = songs.getChildren().take(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)
@ -320,11 +512,38 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private fun getAlbums(
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
artistId: String? = null
type: AlbumListType,
page: Int? = null
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
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>>) {
@ -332,8 +551,8 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
result.detach()
serviceScope.launch {
val playlists = musicService.getPlaylists(true)
playlists.map { playlist ->
val playlists = callWithErrorHandling { musicService.getPlaylists(true) }
playlists?.map { playlist ->
mediaItems.add(
playlist.name,
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
@ -350,28 +569,34 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
result.detach()
serviceScope.launch {
val content = musicService.getPlaylist(id, name)
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
mediaItems.add(
R.string.select_album_play_all,
listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|"),
R.drawable.ic_stat_play_dark,
null,
false
)
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(displayLimit).map { item ->
mediaItems.add(MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry(
item,
listOf(MEDIA_PLAYLIST_SONG_ITEM, id, name, item.id).joinToString("|")
),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
))
// Playlist should be cached as it may contain random elements
playlistCache = content.getAllChild()
playlistCache!!.take(displayLimit).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)
}
result.sendResult(mediaItems)
}
}
@ -379,17 +604,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
serviceScope.launch {
if (playlistCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = musicService.getPlaylist(id, name)
playlistCache = content.getAllChild()
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
playlistCache = content?.getAllChild()
}
mediaPlayerController.download(
playlistCache,
save = false,
autoPlay = true,
playNext = false,
shuffle = false,
newPlaylist = true
)
if (playlistCache != null) playSongs(playlistCache)
}
}
@ -397,88 +615,324 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
serviceScope.launch {
if (playlistCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
val content = musicService.getPlaylist(id, name)
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 content = callWithErrorHandling { musicService.getPlaylist(id, name) }
playlistCache = content?.getAllChild()
}
val song = playlistCache?.firstOrNull{x -> x.id == songId}
if (song != null) playSong(song)
}
}
private fun playAlbum(id: String, name: String) {
serviceScope.launch {
val songs = listSongsInMusicService(id, name)
if (songs != null) playSongs(songs.getAllChild())
}
}
private fun playAlbumSong(id: String, name: String, songId: String) {
serviceScope.launch {
val songs = listSongsInMusicService(id, name)
val song = songs?.getAllChild()?.firstOrNull{x -> x.id == songId}
if (song != null) playSong(song)
}
}
private fun getPodcasts(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
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>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
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>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
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>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
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>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
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>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
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(displayLimit) }
randomSongsCache = content?.getAllChild()
}
if (randomSongsCache != null) playSongs(randomSongsCache)
}
}
private fun getRandomAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
result.sendResult(mediaItems)
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 getRecentAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
result.sendResult(mediaItems)
private fun listSongsInMusicService(id: String, name: String): MusicDirectory? {
return if (!ActiveServerProvider.isOffline() && Util.getShouldUseId3Tags()) {
callWithErrorHandling { musicService.getAlbum(id, name, false) }
} else {
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) }
}
}
private fun getNewestAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
result.sendResult(mediaItems)
}
private fun getFrequentAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach()
result.sendResult(mediaItems)
private fun listStarredSongsInMusicService(): SearchResult? {
return if (Util.getShouldUseId3Tags()) {
callWithErrorHandling { musicService.getStarred2() }
} else {
callWithErrorHandling { musicService.getStarred() }
}
}
private fun MutableList<MediaBrowserCompat.MediaItem>.add(
title: String,
mediaId: String,
icon: Int?,
groupNameId: Int? = null
) {
val builder = MediaDescriptionCompat.Builder()
builder.setTitle(title)
@ -487,6 +941,12 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
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
@ -524,9 +984,54 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
this.add(mediaItem)
}
private fun MutableList<MediaBrowserCompat.MediaItem>.addPlayAllItem(
mediaId: String,
) {
this.add(
R.string.select_album_play_all,
mediaId,
R.drawable.ic_stat_play_dark,
null,
false
)
}
private fun getSectionFromName(name: String): String {
var section = name.first().uppercaseChar()
if (!section.isLetter()) section = '#'
return section.toString()
}
private fun playSongs(songs: List<MusicDirectory.Entry?>?) {
mediaPlayerController.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
}
}
}

View File

@ -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()
}
}

View File

@ -535,7 +535,7 @@ class MediaPlayerService : Service() {
// Init
val context = applicationContext
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
if (notificationBuilder == null) {
@ -654,7 +654,7 @@ class MediaPlayerService : Service() {
else -> return null
}
val pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode)
val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode)
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
}
@ -665,7 +665,7 @@ class MediaPlayerService : Service() {
): NotificationCompat.Action {
val isPlaying = playerState === PlayerState.STARTED
val keycode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
val pendingIntent = getPendingIntentForMediaAction(context, keycode, requestCode)
val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode)
val label: String
val icon: Int
@ -698,7 +698,7 @@ class MediaPlayerService : Service() {
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()
}
@ -710,18 +710,6 @@ class MediaPlayerService : Service() {
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")
companion object {
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"

View File

@ -5,12 +5,10 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN
import android.text.TextUtils
import android.view.KeyEvent
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -75,7 +73,7 @@ class MediaSessionHandler : KoinComponent {
override fun onPlay() {
super.onPlay()
getPendingIntentForMediaAction(
Util.getPendingIntentForMediaAction(
applicationContext,
KeyEvent.KEYCODE_MEDIA_PLAY,
keycode
@ -100,7 +98,7 @@ class MediaSessionHandler : KoinComponent {
override fun onPause() {
super.onPause()
getPendingIntentForMediaAction(
Util.getPendingIntentForMediaAction(
applicationContext,
KeyEvent.KEYCODE_MEDIA_PAUSE,
keycode
@ -110,7 +108,7 @@ class MediaSessionHandler : KoinComponent {
override fun onStop() {
super.onStop()
getPendingIntentForMediaAction(
Util.getPendingIntentForMediaAction(
applicationContext,
KeyEvent.KEYCODE_MEDIA_STOP,
keycode
@ -120,7 +118,7 @@ class MediaSessionHandler : KoinComponent {
override fun onSkipToNext() {
super.onSkipToNext()
getPendingIntentForMediaAction(
Util.getPendingIntentForMediaAction(
applicationContext,
KeyEvent.KEYCODE_MEDIA_NEXT,
keycode
@ -130,7 +128,7 @@ class MediaSessionHandler : KoinComponent {
override fun onSkipToPrevious() {
super.onSkipToPrevious()
getPendingIntentForMediaAction(
Util.getPendingIntentForMediaAction(
applicationContext,
KeyEvent.KEYCODE_MEDIA_PREVIOUS,
keycode
@ -248,7 +246,6 @@ class MediaSessionHandler : KoinComponent {
cachedPlaylist = playlist
if (mediaSession == null) return
// TODO Implement Now Playing queue handling properly
mediaSession!!.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing))
mediaSession!!.setQueue(playlist.mapIndexed { id, song ->
MediaSessionCompat.QueueItem(
@ -306,17 +303,4 @@ class MediaSessionHandler : KoinComponent {
private fun unregisterMediaButtonEventReceiver() {
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)
}
}

View File

@ -21,6 +21,7 @@ package org.moire.ultrasonic.util
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.content.DialogInterface
@ -37,15 +38,18 @@ import android.net.Uri
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.WifiLock
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Parcelable
import android.support.v4.media.MediaDescriptionCompat
import android.text.TextUtils
import android.util.TypedValue
import android.view.Gravity
import android.view.KeyEvent
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.annotation.AnyRes
import androidx.media.utils.MediaConstants
import androidx.preference.PreferenceManager
import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
@ -641,12 +645,13 @@ object Util {
}
@JvmStatic
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark>): MusicDirectory {
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark?>): MusicDirectory {
val musicDirectory = MusicDirectory()
var song: MusicDirectory.Entry
for ((position, _, _, _, _, entry) in bookmarks) {
song = entry
song.bookmarkPosition = position
for (bookmark in bookmarks) {
if (bookmark == null) continue
song = bookmark.entry
song.bookmarkPosition = bookmark.position
musicDirectory.addChild(song)
}
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 artist = StringBuilder(60)
@ -1266,7 +1275,7 @@ object Util {
artist.append(String.format("%s ", formatTotalDuration(duration.toLong())))
}
if (song.bitRate != null)
if (song.bitRate != null && song.bitRate!! > 0)
bitRate = String.format(
appContext().getString(R.string.song_details_kbps), song.bitRate
)
@ -1282,7 +1291,7 @@ object Util {
val artistName = song.artist
if (artistName != null) {
if (shouldDisplayBitrateWithArtist()) {
if (shouldDisplayBitrateWithArtist() && (!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank())) {
artist.append(artistName).append(" (").append(
String.format(
appContext().getString(R.string.song_details_all),
@ -1311,10 +1320,28 @@ object Util {
).append(')')
}
if (groupNameId != null)
descriptionBuilder.setExtras(Bundle().apply { putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
appContext().getString(groupNameId)
) })
descriptionBuilder.setTitle(title)
descriptionBuilder.setSubtitle(artist)
descriptionBuilder.setMediaId(mediaId)
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)
}
}