mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-05-06 18:41:05 +03:00
Started implementing Media Browser
Added root menus, playlists and artists
This commit is contained in:
parent
635ea2f55e
commit
f50d6f13f4
File diff suppressed because it is too large
Load Diff
@ -313,7 +313,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
|||||||
}
|
}
|
||||||
|
|
||||||
repeatButton.setOnClickListener {
|
repeatButton.setOnClickListener {
|
||||||
val repeatMode = mediaPlayerController.repeatMode?.next()
|
val repeatMode = mediaPlayerController.repeatMode.next()
|
||||||
mediaPlayerController.repeatMode = repeatMode
|
mediaPlayerController.repeatMode = repeatMode
|
||||||
onDownloadListChanged()
|
onDownloadListChanged()
|
||||||
when (repeatMode) {
|
when (repeatMode) {
|
||||||
|
@ -7,26 +7,65 @@ import android.support.v4.media.MediaDescriptionCompat
|
|||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
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
|
||||||
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
const val MEDIA_ROOT_ID = "MEDIA_ROOT_ID"
|
||||||
|
const val MEDIA_ALBUM_ID = "MEDIA_ALBUM_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"
|
||||||
|
const val MEDIA_ALBUM_RANDOM_ID = "MEDIA_ALBUM_RANDOM_ID"
|
||||||
|
const val MEDIA_ALBUM_STARRED_ID = "MEDIA_ALBUM_STARRED_ID"
|
||||||
|
const val MEDIA_SONG_RANDOM_ID = "MEDIA_SONG_RANDOM_ID"
|
||||||
|
const val MEDIA_SONG_STARRED_ID = "MEDIA_SONG_STARRED_ID"
|
||||||
|
const val MEDIA_ARTIST_ID = "MEDIA_ARTIST_ID"
|
||||||
|
const val MEDIA_LIBRARY_ID = "MEDIA_LIBRARY_ID"
|
||||||
|
const val MEDIA_PLAYLIST_ID = "MEDIA_PLAYLIST_ID"
|
||||||
|
const val MEDIA_SHARE_ID = "MEDIA_SHARE_ID"
|
||||||
|
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_ARTIST_ITEM = "MEDIA_ARTIST_ITEM"
|
||||||
|
const val MEDIA_ARTIST_SECTION = "MEDIA_ARTIST_SECTION"
|
||||||
|
|
||||||
const val MY_MEDIA_ROOT_ID = "MY_MEDIA_ROOT_ID"
|
// Currently the display limit for long lists is 100 items
|
||||||
const val MY_MEDIA_ALBUM_ID = "MY_MEDIA_ALBUM_ID"
|
const val displayLimit = 100
|
||||||
const val MY_MEDIA_ARTIST_ID = "MY_MEDIA_ARTIST_ID"
|
|
||||||
const val MY_MEDIA_ALBUM_ITEM = "MY_MEDIA_ALBUM_ITEM"
|
|
||||||
const val MY_MEDIA_LIBRARY_ID = "MY_MEDIA_LIBRARY_ID"
|
|
||||||
const val MY_MEDIA_PLAYLIST_ID = "MY_MEDIA_PLAYLIST_ID"
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MediaBrowserService implementation for e.g. Android Auto
|
||||||
|
*/
|
||||||
class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
||||||
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
|
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
|
||||||
private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>()
|
private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>()
|
||||||
private val mediaSessionHandler by inject<MediaSessionHandler>()
|
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 serviceJob = Job()
|
||||||
|
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||||
|
|
||||||
|
private var playlistCache: List<MusicDirectory.Entry>? = null
|
||||||
|
|
||||||
|
private val isOffline get() = ActiveServerProvider.isOffline()
|
||||||
|
private val useId3Tags get() = Util.getShouldUseId3Tags()
|
||||||
|
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@ -39,7 +78,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) {
|
override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) {
|
||||||
// TODO implement
|
Timber.d("AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s", mediaId)
|
||||||
|
|
||||||
|
if (mediaId == null) return
|
||||||
|
val mediaIdParts = mediaId.split('|')
|
||||||
|
|
||||||
|
when (mediaIdParts.first()) {
|
||||||
|
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
|
||||||
|
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(mediaIdParts[1], mediaIdParts[2], mediaIdParts[3])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {
|
override fun onPlayFromSearchRequested(query: String?, extras: Bundle?) {
|
||||||
@ -65,6 +112,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
|
||||||
mediaSessionHandler.release()
|
mediaSessionHandler.release()
|
||||||
|
serviceJob.cancel()
|
||||||
|
|
||||||
Timber.i("AutoMediaBrowserService onDestroy finished")
|
Timber.i("AutoMediaBrowserService onDestroy finished")
|
||||||
}
|
}
|
||||||
@ -73,20 +121,8 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
clientPackageName: String,
|
clientPackageName: String,
|
||||||
clientUid: Int,
|
clientUid: Int,
|
||||||
rootHints: Bundle?
|
rootHints: Bundle?
|
||||||
): BrowserRoot? {
|
): BrowserRoot {
|
||||||
Timber.d("AutoMediaBrowserService onGetRoot called")
|
Timber.d("AutoMediaBrowserService onGetRoot called. clientPackageName: %s; clientUid: %d", clientPackageName, clientUid)
|
||||||
|
|
||||||
// TODO: The number of horizontal items available on the Andoid Auto screen. Check and handle.
|
|
||||||
val maximumRootChildLimit = rootHints!!.getInt(
|
|
||||||
MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
|
|
||||||
4
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: The type of the horizontal items children on the Android Auto screen. Check and handle.
|
|
||||||
val supportedRootChildFlags = rootHints!!.getInt(
|
|
||||||
MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
val extras = Bundle()
|
val extras = Bundle()
|
||||||
extras.putInt(
|
extras.putInt(
|
||||||
@ -96,19 +132,37 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
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)
|
||||||
|
|
||||||
return BrowserRoot(MY_MEDIA_ROOT_ID, extras)
|
return BrowserRoot(MEDIA_ROOT_ID, extras)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadChildren(
|
override fun onLoadChildren(
|
||||||
parentId: String,
|
parentId: String,
|
||||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
||||||
) {
|
) {
|
||||||
Timber.d("AutoMediaBrowserService onLoadChildren called")
|
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
|
||||||
|
|
||||||
if (parentId == MY_MEDIA_ROOT_ID) {
|
val parentIdParts = parentId.split('|')
|
||||||
return getRootItems(result)
|
|
||||||
} else {
|
when (parentIdParts.first()) {
|
||||||
return getAlbumLists(result)
|
MEDIA_ROOT_ID -> return getRootItems(result)
|
||||||
|
MEDIA_LIBRARY_ID -> return getLibrary(result)
|
||||||
|
MEDIA_ARTIST_ID -> return getArtists(result)
|
||||||
|
MEDIA_ARTIST_SECTION -> return getArtists(result, parentIdParts[1])
|
||||||
|
MEDIA_ALBUM_ID -> return getAlbums(result)
|
||||||
|
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_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])
|
||||||
|
else -> result.sendResult(mutableListOf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,70 +172,361 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
||||||
) {
|
) {
|
||||||
super.onSearch(query, extras, result)
|
super.onSearch(query, extras, result)
|
||||||
|
// TODO implement
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
// TODO implement this with proper texts, icons, etc
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
MediaBrowserCompat.MediaItem(
|
R.string.music_library_label,
|
||||||
MediaDescriptionCompat.Builder()
|
MEDIA_LIBRARY_ID,
|
||||||
.setTitle("Library")
|
R.drawable.ic_library,
|
||||||
.setMediaId(MY_MEDIA_LIBRARY_ID)
|
null
|
||||||
.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
MediaBrowserCompat.MediaItem(
|
R.string.main_artists_title,
|
||||||
MediaDescriptionCompat.Builder()
|
MEDIA_ARTIST_ID,
|
||||||
.setTitle("Artists")
|
R.drawable.ic_artist,
|
||||||
.setMediaId(MY_MEDIA_ARTIST_ID)
|
null
|
||||||
.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
MediaBrowserCompat.MediaItem(
|
R.string.main_albums_title,
|
||||||
MediaDescriptionCompat.Builder()
|
MEDIA_ALBUM_ID,
|
||||||
.setTitle("Albums")
|
R.drawable.ic_menu_browse_dark,
|
||||||
.setMediaId(MY_MEDIA_ALBUM_ID)
|
null
|
||||||
.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
MediaBrowserCompat.MediaItem(
|
R.string.playlist_label,
|
||||||
MediaDescriptionCompat.Builder()
|
MEDIA_PLAYLIST_ID,
|
||||||
.setTitle("Playlists")
|
R.drawable.ic_menu_playlists_dark,
|
||||||
.setMediaId(MY_MEDIA_PLAYLIST_ID)
|
null
|
||||||
.build(),
|
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
result.sendResult(mediaItems)
|
result.sendResult(mediaItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAlbumLists(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
private fun getLibrary(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
|
||||||
val description = MediaDescriptionCompat.Builder()
|
// Songs
|
||||||
.setTitle("Test")
|
mediaItems.add(
|
||||||
.setMediaId(MY_MEDIA_ALBUM_ITEM + 1)
|
R.string.main_songs_random,
|
||||||
.build()
|
MEDIA_SONG_RANDOM_ID,
|
||||||
|
null,
|
||||||
|
R.string.main_songs_title
|
||||||
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
MediaBrowserCompat.MediaItem(
|
R.string.main_songs_starred,
|
||||||
description,
|
MEDIA_SONG_STARRED_ID,
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
null,
|
||||||
|
R.string.main_songs_title
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Albums
|
||||||
|
mediaItems.add(
|
||||||
|
R.string.main_albums_newest,
|
||||||
|
MEDIA_ALBUM_NEWEST_ID,
|
||||||
|
null,
|
||||||
|
R.string.main_albums_title
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mediaItems.add(
|
||||||
|
R.string.main_albums_recent,
|
||||||
|
MEDIA_ALBUM_RECENT_ID,
|
||||||
|
null,
|
||||||
|
R.string.main_albums_title
|
||||||
|
)
|
||||||
|
|
||||||
|
mediaItems.add(
|
||||||
|
R.string.main_albums_frequent,
|
||||||
|
MEDIA_ALBUM_FREQUENT_ID,
|
||||||
|
null,
|
||||||
|
R.string.main_albums_title
|
||||||
|
)
|
||||||
|
|
||||||
|
mediaItems.add(
|
||||||
|
R.string.main_albums_random,
|
||||||
|
MEDIA_ALBUM_RANDOM_ID,
|
||||||
|
null,
|
||||||
|
R.string.main_albums_title
|
||||||
|
)
|
||||||
|
|
||||||
|
mediaItems.add(
|
||||||
|
R.string.main_albums_starred,
|
||||||
|
MEDIA_ALBUM_STARRED_ID,
|
||||||
|
null,
|
||||||
|
R.string.main_albums_title
|
||||||
|
)
|
||||||
|
|
||||||
|
// Other
|
||||||
|
mediaItems.add(R.string.button_bar_shares, MEDIA_SHARE_ID, null, null)
|
||||||
|
mediaItems.add(R.string.button_bar_bookmarks, MEDIA_BOOKMARK_ID, null, null)
|
||||||
|
mediaItems.add(R.string.button_bar_podcasts, MEDIA_PODCAST_ID, null, null)
|
||||||
|
|
||||||
result.sendResult(mediaItems)
|
result.sendResult(mediaItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getArtists(result: Result<MutableList<MediaBrowserCompat.MediaItem>>, section: String? = null) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
|
||||||
|
serviceScope.launch {
|
||||||
|
var artists = if (!isOffline && useId3Tags) {
|
||||||
|
// TODO this list can be big so we're not refreshing.
|
||||||
|
// Maybe a refresh menu item can be added
|
||||||
|
musicService.getArtists(false)
|
||||||
|
} else {
|
||||||
|
musicService.getIndexes(musicFolderId, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
mediaItems.add(
|
||||||
|
currentSection,
|
||||||
|
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
artists.map { artist ->
|
||||||
|
mediaItems.add(
|
||||||
|
artist.name ?: "",
|
||||||
|
listOf(MEDIA_ARTIST_ITEM, artist.id).joinToString("|"),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAlbums(
|
||||||
|
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
|
||||||
|
artistId: String? = null
|
||||||
|
) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPlaylists(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
|
||||||
|
serviceScope.launch {
|
||||||
|
val playlists = musicService.getPlaylists(true)
|
||||||
|
playlists.map { playlist ->
|
||||||
|
mediaItems.add(
|
||||||
|
playlist.name,
|
||||||
|
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
|
||||||
|
.joinToString("|"),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPlaylist(id: String, name: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
|
||||||
|
serviceScope.launch {
|
||||||
|
val content = 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
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playPlaylist(id: String, name: String) {
|
||||||
|
serviceScope.launch {
|
||||||
|
if (playlistCache == null) {
|
||||||
|
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
|
||||||
|
val content = musicService.getPlaylist(id, name)
|
||||||
|
playlistCache = content.getAllChild()
|
||||||
|
}
|
||||||
|
mediaPlayerController.download(
|
||||||
|
playlistCache,
|
||||||
|
save = false,
|
||||||
|
autoPlay = true,
|
||||||
|
playNext = false,
|
||||||
|
shuffle = false,
|
||||||
|
newPlaylist = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playPlaylistSong(id: String, name: String, songId: String) {
|
||||||
|
serviceScope.launch {
|
||||||
|
if (playlistCache == null) {
|
||||||
|
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
|
||||||
|
val content = 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPodcasts(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBookmarks(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getShares(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStarredSongs(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRandomSongs(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStarredAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRandomAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRecentAlbums(result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
result.detach()
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 MutableList<MediaBrowserCompat.MediaItem>.add(
|
||||||
|
title: String,
|
||||||
|
mediaId: String,
|
||||||
|
icon: Int?,
|
||||||
|
) {
|
||||||
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
|
builder.setTitle(title)
|
||||||
|
builder.setMediaId(mediaId)
|
||||||
|
|
||||||
|
if (icon != null)
|
||||||
|
builder.setIconUri(Util.getUriToDrawable(applicationContext, icon))
|
||||||
|
|
||||||
|
val mediaItem = MediaBrowserCompat.MediaItem(
|
||||||
|
builder.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
this.add(mediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableList<MediaBrowserCompat.MediaItem>.add(
|
||||||
|
resId: Int,
|
||||||
|
mediaId: String,
|
||||||
|
icon: Int?,
|
||||||
|
groupNameId: Int?,
|
||||||
|
browsable: Boolean = true
|
||||||
|
) {
|
||||||
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
|
builder.setTitle(getString(resId))
|
||||||
|
builder.setMediaId(mediaId)
|
||||||
|
|
||||||
|
if (icon != null)
|
||||||
|
builder.setIconUri(Util.getUriToDrawable(applicationContext, icon))
|
||||||
|
|
||||||
|
if (groupNameId != null)
|
||||||
|
builder.setExtras(Bundle().apply { putString(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||||
|
getString(groupNameId)
|
||||||
|
) })
|
||||||
|
|
||||||
|
val mediaItem = MediaBrowserCompat.MediaItem(
|
||||||
|
builder.build(),
|
||||||
|
if (browsable) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
this.add(mediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSectionFromName(name: String): String {
|
||||||
|
var section = name.first().uppercaseChar()
|
||||||
|
if (!section.isLetter()) section = '#'
|
||||||
|
return section.toString()
|
||||||
|
}
|
||||||
}
|
}
|
@ -247,10 +247,10 @@ class MediaPlayerController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@set:Synchronized
|
@set:Synchronized
|
||||||
var repeatMode: RepeatMode?
|
var repeatMode: RepeatMode
|
||||||
get() = Util.getRepeatMode()
|
get() = Util.repeatMode
|
||||||
set(repeatMode) {
|
set(repeatMode) {
|
||||||
Util.setRepeatMode(repeatMode)
|
Util.repeatMode = repeatMode
|
||||||
val mediaPlayerService = runningInstance
|
val mediaPlayerService = runningInstance
|
||||||
mediaPlayerService?.setNextPlaying()
|
mediaPlayerService?.setNextPlaying()
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ class MediaPlayerService : Service() {
|
|||||||
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
||||||
|
|
||||||
private val repeatMode: RepeatMode
|
private val repeatMode: RepeatMode
|
||||||
get() = Util.getRepeatMode()
|
get() = Util.repeatMode
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
return binder
|
return binder
|
||||||
|
@ -40,13 +40,13 @@ class ShareHandler(val context: Context) {
|
|||||||
swipe: SwipeRefreshLayout?,
|
swipe: SwipeRefreshLayout?,
|
||||||
cancellationToken: CancellationToken
|
cancellationToken: CancellationToken
|
||||||
) {
|
) {
|
||||||
val askForDetails = Util.getShouldAskForShareDetails()
|
val askForDetails = Util.shouldAskForShareDetails
|
||||||
val shareDetails = ShareDetails()
|
val shareDetails = ShareDetails()
|
||||||
shareDetails.Entries = entries
|
shareDetails.Entries = entries
|
||||||
if (askForDetails) {
|
if (askForDetails) {
|
||||||
showDialog(fragment, shareDetails, swipe, cancellationToken)
|
showDialog(fragment, shareDetails, swipe, cancellationToken)
|
||||||
} else {
|
} else {
|
||||||
shareDetails.Description = Util.getDefaultShareDescription()
|
shareDetails.Description = Util.defaultShareDescription
|
||||||
shareDetails.Expiration = TimeSpan.getCurrentTime().add(
|
shareDetails.Expiration = TimeSpan.getCurrentTime().add(
|
||||||
Util.getDefaultShareExpirationInMillis(context)
|
Util.getDefaultShareExpirationInMillis(context)
|
||||||
).totalMilliseconds
|
).totalMilliseconds
|
||||||
@ -133,16 +133,16 @@ class ShareHandler(val context: Context) {
|
|||||||
}
|
}
|
||||||
shareDetails.Description = shareDescription!!.text.toString()
|
shareDetails.Description = shareDescription!!.text.toString()
|
||||||
if (hideDialogCheckBox!!.isChecked) {
|
if (hideDialogCheckBox!!.isChecked) {
|
||||||
Util.setShouldAskForShareDetails(false)
|
Util.shouldAskForShareDetails = false
|
||||||
}
|
}
|
||||||
if (saveAsDefaultsCheckBox!!.isChecked) {
|
if (saveAsDefaultsCheckBox!!.isChecked) {
|
||||||
val timeSpanType: String = timeSpanPicker!!.timeSpanType
|
val timeSpanType: String = timeSpanPicker!!.timeSpanType
|
||||||
val timeSpanAmount: Int = timeSpanPicker!!.timeSpanAmount
|
val timeSpanAmount: Int = timeSpanPicker!!.timeSpanAmount
|
||||||
Util.setDefaultShareExpiration(
|
Util.defaultShareExpiration =
|
||||||
if (!noExpirationCheckBox!!.isChecked && timeSpanAmount > 0)
|
if (!noExpirationCheckBox!!.isChecked && timeSpanAmount > 0)
|
||||||
String.format("%d:%s", timeSpanAmount, timeSpanType) else ""
|
String.format("%d:%s", timeSpanAmount, timeSpanType) else ""
|
||||||
)
|
|
||||||
Util.setDefaultShareDescription(shareDetails.Description)
|
Util.defaultShareDescription = shareDetails.Description
|
||||||
}
|
}
|
||||||
share(fragment, shareDetails, swipe, cancellationToken)
|
share(fragment, shareDetails, swipe, cancellationToken)
|
||||||
}
|
}
|
||||||
@ -157,8 +157,8 @@ class ShareHandler(val context: Context) {
|
|||||||
b ->
|
b ->
|
||||||
timeSpanPicker!!.isEnabled = !b
|
timeSpanPicker!!.isEnabled = !b
|
||||||
}
|
}
|
||||||
val defaultDescription = Util.getDefaultShareDescription()
|
val defaultDescription = Util.defaultShareDescription
|
||||||
val timeSpan = Util.getDefaultShareExpiration()
|
val timeSpan = Util.defaultShareExpiration
|
||||||
val split = pattern.split(timeSpan)
|
val split = pattern.split(timeSpan)
|
||||||
if (split.size == 2) {
|
if (split.size == 2) {
|
||||||
val timeSpanAmount = split[0].toInt()
|
val timeSpanAmount = split[0].toInt()
|
||||||
|
@ -24,6 +24,9 @@ import timber.log.Timber
|
|||||||
|
|
||||||
private const val INTENT_CODE_MEDIA_BUTTON = 161
|
private const val INTENT_CODE_MEDIA_BUTTON = 161
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central place to handle the state of the MediaSession
|
||||||
|
*/
|
||||||
class MediaSessionHandler : KoinComponent {
|
class MediaSessionHandler : KoinComponent {
|
||||||
|
|
||||||
private var mediaSession: MediaSessionCompat? = null
|
private var mediaSession: MediaSessionCompat? = null
|
||||||
@ -249,7 +252,7 @@ class MediaSessionHandler : KoinComponent {
|
|||||||
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(
|
||||||
getMediaDescriptionForEntry(song),
|
Util.getMediaDescriptionForEntry(song),
|
||||||
id.toLong())
|
id.toLong())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -316,66 +319,4 @@ class MediaSessionHandler : KoinComponent {
|
|||||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
|
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
|
||||||
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMediaDescriptionForEntry(song: MusicDirectory.Entry): MediaDescriptionCompat {
|
|
||||||
|
|
||||||
val descriptionBuilder = MediaDescriptionCompat.Builder()
|
|
||||||
val artist = StringBuilder(60)
|
|
||||||
var bitRate: String? = null
|
|
||||||
|
|
||||||
val duration = song.duration
|
|
||||||
if (duration != null) {
|
|
||||||
artist.append(String.format("%s ", Util.formatTotalDuration(duration.toLong())))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (song.bitRate != null)
|
|
||||||
bitRate = String.format(
|
|
||||||
applicationContext.getString(R.string.song_details_kbps), song.bitRate
|
|
||||||
)
|
|
||||||
|
|
||||||
val fileFormat: String?
|
|
||||||
val suffix = song.suffix
|
|
||||||
val transcodedSuffix = song.transcodedSuffix
|
|
||||||
|
|
||||||
fileFormat = if (
|
|
||||||
TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo
|
|
||||||
) suffix else String.format("%s > %s", suffix, transcodedSuffix)
|
|
||||||
|
|
||||||
val artistName = song.artist
|
|
||||||
|
|
||||||
if (artistName != null) {
|
|
||||||
if (Util.shouldDisplayBitrateWithArtist()) {
|
|
||||||
artist.append(artistName).append(" (").append(
|
|
||||||
String.format(
|
|
||||||
applicationContext.getString(R.string.song_details_all),
|
|
||||||
if (bitRate == null) "" else String.format("%s ", bitRate), fileFormat
|
|
||||||
)
|
|
||||||
).append(')')
|
|
||||||
} else {
|
|
||||||
artist.append(artistName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val trackNumber = song.track ?: 0
|
|
||||||
|
|
||||||
val title = StringBuilder(60)
|
|
||||||
if (Util.shouldShowTrackNumber() && trackNumber > 0)
|
|
||||||
title.append(String.format("%02d - ", trackNumber))
|
|
||||||
|
|
||||||
title.append(song.title)
|
|
||||||
|
|
||||||
if (song.isVideo && Util.shouldDisplayBitrateWithArtist()) {
|
|
||||||
title.append(" (").append(
|
|
||||||
String.format(
|
|
||||||
applicationContext.getString(R.string.song_details_all),
|
|
||||||
if (bitRate == null) "" else String.format("%s ", bitRate), fileFormat
|
|
||||||
)
|
|
||||||
).append(')')
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptionBuilder.setTitle(title)
|
|
||||||
descriptionBuilder.setSubtitle(artist)
|
|
||||||
|
|
||||||
return descriptionBuilder.build()
|
|
||||||
}
|
|
||||||
}
|
}
|
1320
ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
Normal file
1320
ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt
Normal file
File diff suppressed because it is too large
Load Diff
10
ultrasonic/src/main/res/drawable/ic_artist.xml
Normal file
10
ultrasonic/src/main/res/drawable/ic_artist.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M9,13.75c-2.34,0 -7,1.17 -7,3.5L2,19h14v-1.75c0,-2.33 -4.66,-3.5 -7,-3.5zM4.34,17c0.84,-0.58 2.87,-1.25 4.66,-1.25s3.82,0.67 4.66,1.25L4.34,17zM9,12c1.93,0 3.5,-1.57 3.5,-3.5S10.93,5 9,5 5.5,6.57 5.5,8.5 7.07,12 9,12zM9,7c0.83,0 1.5,0.67 1.5,1.5S9.83,10 9,10s-1.5,-0.67 -1.5,-1.5S8.17,7 9,7zM16.04,13.81c1.16,0.84 1.96,1.96 1.96,3.44L18,19h4v-1.75c0,-2.02 -3.5,-3.17 -5.96,-3.44zM15,12c1.93,0 3.5,-1.57 3.5,-3.5S16.93,5 15,5c-0.54,0 -1.04,0.13 -1.5,0.35 0.63,0.89 1,1.98 1,3.15s-0.37,2.26 -1,3.15c0.46,0.22 0.96,0.35 1.5,0.35z"/>
|
||||||
|
</vector>
|
11
ultrasonic/src/main/res/drawable/ic_library.xml
Normal file
11
ultrasonic/src/main/res/drawable/ic_library.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M22,6h-5v8.18C16.69,14.07 16.35,14 16,14c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3V8h3V6zM15,6H3v2h12V6zM15,10H3v2h12V10zM11,14H3v2h8V14z"/>
|
||||||
|
</vector>
|
Loading…
x
Reference in New Issue
Block a user