Complete android auto support (#47)

This commit is contained in:
vfsfitvnm 2022-10-11 09:08:24 +02:00
parent 6fb8e41a04
commit 270986215c
13 changed files with 329 additions and 386 deletions

View file

@ -51,7 +51,6 @@ import it.vfsfitvnm.vimusic.models.SongArtistMap
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@Dao
interface Database {
@ -127,10 +126,6 @@ interface Database {
@Query("SELECT * FROM Song WHERE id = :id")
fun song(id: String): Flow<Song?>
@Transaction
@Query("SELECT * FROM Song WHERE id = :id")
fun songById(id: String): Flow<DetailedSong?>
@Query("SELECT likedAt FROM Song WHERE id = :songId")
fun likedAt(songId: String): Flow<Long?>
@ -238,28 +233,44 @@ interface Database {
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY name ASC")
fun playlistPreviewsByName(): Flow<List<PlaylistPreview>>
fun playlistPreviewsByNameAsc(): Flow<List<PlaylistPreview>>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY ROWID ASC")
fun playlistPreviewsByDateAdded(): Flow<List<PlaylistPreview>>
fun playlistPreviewsByDateAddedAsc(): Flow<List<PlaylistPreview>>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY songCount ASC")
fun playlistPreviewsByDateSongCount(): Flow<List<PlaylistPreview>>
fun playlistPreviewsByDateSongCountAsc(): Flow<List<PlaylistPreview>>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY name DESC")
fun playlistPreviewsByNameDesc(): Flow<List<PlaylistPreview>>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY ROWID DESC")
fun playlistPreviewsByDateAddedDesc(): Flow<List<PlaylistPreview>>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY songCount DESC")
fun playlistPreviewsByDateSongCountDesc(): Flow<List<PlaylistPreview>>
fun playlistPreviews(
sortBy: PlaylistSortBy,
sortOrder: SortOrder
): Flow<List<PlaylistPreview>> {
return when (sortBy) {
PlaylistSortBy.Name -> playlistPreviewsByName()
PlaylistSortBy.DateAdded -> playlistPreviewsByDateAdded()
PlaylistSortBy.SongCount -> playlistPreviewsByDateSongCount()
}.map {
when (sortOrder) {
SortOrder.Ascending -> it
SortOrder.Descending -> it.reversed()
PlaylistSortBy.Name -> when (sortOrder) {
SortOrder.Ascending -> playlistPreviewsByNameAsc()
SortOrder.Descending -> playlistPreviewsByNameDesc()
}
PlaylistSortBy.SongCount -> when (sortOrder) {
SortOrder.Ascending -> playlistPreviewsByDateSongCountAsc()
SortOrder.Descending -> playlistPreviewsByDateSongCountDesc()
}
PlaylistSortBy.DateAdded -> when (sortOrder) {
SortOrder.Ascending -> playlistPreviewsByDateAddedAsc()
SortOrder.Descending -> playlistPreviewsByDateAddedDesc()
}
}
}

View file

@ -1,16 +0,0 @@
package it.vfsfitvnm.vimusic.enums
enum class MediaIDType {
Playlist,
RandomFavorites,
RandomSongs,
Song;
val prefix: String
get() = when (this) {
Song -> "VIMUSIC_SONG_ID_"
Playlist -> "VIMUSIC_PLAYLIST_ID_"
RandomSongs -> "VIMUSIC_RANDOM_SONGS"
RandomFavorites -> "VIMUSIC_RANDOM_FAVORITES"
}
}

View file

@ -1,206 +1,307 @@
package it.vfsfitvnm.vimusic.service
import android.media.MediaDescription as BrowserMediaDescription
import android.media.browse.MediaBrowser.MediaItem as BrowserMediaItem
import android.content.ComponentName
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.media.MediaDescription
import android.media.browse.MediaBrowser.MediaItem
import android.media.session.MediaSession
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import android.os.Process
import android.service.media.MediaBrowserService
import it.vfsfitvnm.vimusic.BuildConfig
import androidx.annotation.DrawableRes
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.media3.common.Player
import androidx.media3.datasource.cache.Cache
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.utils.MediaIDHelper
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.PlaylistPreview
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious
import it.vfsfitvnm.vimusic.utils.intent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class PlayerMediaBrowserService : MediaBrowserService() {
class PlayerMediaBrowserService : MediaBrowserService(), ServiceConnection {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private var lastSongs = emptyList<DetailedSong>()
var playerServiceBinder: PlayerService.Binder? = null
var isBound = false
private var bound = false
override fun onCreate() {
super.onCreate()
val intent = Intent(this, PlayerService::class.java)
bindService(intent, playerConnection, Context.BIND_AUTO_CREATE)
override fun onDestroy() {
if (bound) {
unbindService(this)
}
super.onDestroy()
}
override fun onServiceConnected(className: ComponentName, service: IBinder) {
if (service is PlayerService.Binder) {
bound = true
sessionToken = service.mediaSession.sessionToken
service.mediaSession.setCallback(SessionCallback(service.player, service.cache))
}
}
override fun onServiceDisconnected(name: ComponentName) = Unit
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): BrowserRoot? {
if (!isCallerAllowed(clientPackageName, clientUid)) {
return null
}
val extras = Bundle()
extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
return BrowserRoot(MEDIA_ROOT_ID, extras)
}
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaItem>>
) {
when (parentId) {
MEDIA_ROOT_ID -> result.sendResult(createMenuMediaItem())
MEDIA_PLAYLISTS_ID -> result.sendResult(createPlaylistsMediaItem())
MEDIA_FAVORITES_ID -> result.sendResult(createFavoritesMediaItem())
MEDIA_SONGS_ID -> result.sendResult(createSongsMediaItem())
}
}
private fun createFavoritesMediaItem(): MutableList<MediaItem> {
val favorites = runBlocking(Dispatchers.IO) {
Database.favorites().first()
}.map { entry ->
MediaItem(
MediaDescription.Builder()
.setMediaId(MediaIDHelper.createMediaIdForSong(entry.id))
.setTitle(entry.title)
.setSubtitle(entry.artistsText)
.setIconUri(
Uri.parse(entry.thumbnailUrl)
)
.build(), MediaItem.FLAG_PLAYABLE
)
}.toCollection(mutableListOf())
if (favorites.isNotEmpty()) {
favorites.add(
0, MediaItem(
MediaDescription.Builder()
.setMediaId(MediaIDHelper.createMediaIdForRandomFavorites())
.setTitle("Play all random")
.setIconUri(
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/shuffle")
)
.build(), MediaItem.FLAG_PLAYABLE
)
)
}
return favorites
}
private fun createSongsMediaItem(): MutableList<MediaItem> {
val songs = runBlocking(Dispatchers.IO) {
Database.songs(SongSortBy.DateAdded, SortOrder.Descending).first()
}.map { entry ->
MediaItem(
MediaDescription.Builder()
.setMediaId(MediaIDHelper.createMediaIdForSong(entry.id))
.setTitle(entry.title)
.setSubtitle(entry.artistsText)
.setIconUri(
Uri.parse(entry.thumbnailUrl)
)
.build(), MediaItem.FLAG_PLAYABLE
)
}.toCollection(mutableListOf())
if (songs.isNotEmpty()) {
songs.add(
0, MediaItem(
MediaDescription.Builder()
.setMediaId(MediaIDHelper.createMediaIdForRandomSongs())
.setTitle("Play all random")
.setIconUri(
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/shuffle")
)
.build(), MediaItem.FLAG_PLAYABLE
)
)
}
return songs
}
private fun createPlaylistsMediaItem(): MutableList<MediaItem> {
return runBlocking(Dispatchers.IO) {
Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending).first()
}.map { entry ->
MediaItem(
MediaDescription.Builder()
.setMediaId(MediaIDHelper.createMediaIdForPlaylist(entry.playlist.id))
.setTitle(entry.playlist.name)
.setSubtitle("${entry.songCount} songs")
.setIconUri(
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/playlist")
)
.build(), MediaItem.FLAG_PLAYABLE
)
}.toCollection(mutableListOf())
}
private fun createMenuMediaItem(): MutableList<MediaItem> {
return mutableListOf(
MediaItem(
MediaDescription.Builder()
.setMediaId(MEDIA_PLAYLISTS_ID)
.setTitle("Playlists")
.setIconUri(
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/playlist_white")
)
.build(), MediaItem.FLAG_BROWSABLE
), MediaItem(
MediaDescription.Builder()
.setMediaId(MEDIA_FAVORITES_ID)
.setTitle("Favorites")
.setIconUri(
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/heart_white")
)
.build(), MediaItem.FLAG_BROWSABLE
), MediaItem(
MediaDescription.Builder()
.setMediaId(MEDIA_SONGS_ID)
.setTitle("Songs")
.setIconUri(
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/disc_white")
)
.build(), MediaItem.FLAG_BROWSABLE
)
)
}
private val playerConnection = object : ServiceConnection {
override fun onServiceConnected(
className: ComponentName,
service: IBinder
return if (clientUid == Process.myUid()
|| clientUid == Process.SYSTEM_UID
|| clientPackageName == "com.google.android.projection.gearhead"
) {
playerServiceBinder = service as PlayerService.Binder
isBound = true
sessionToken = playerServiceBinder?.mediaSession?.sessionToken
}
override fun onServiceDisconnected(name: ComponentName) {
isBound = false
bindService(intent<PlayerService>(), this, Context.BIND_AUTO_CREATE)
BrowserRoot(
MediaId.root,
bundleOf("android.media.browse.CONTENT_STYLE_BROWSABLE_HINT" to 1)
)
} else {
null
}
}
private fun isCallerAllowed(
clientPackageName: String,
clientUid: Int
): Boolean {
return when {
clientUid == Process.myUid() -> true
clientUid == Process.SYSTEM_UID -> true
ANDROID_AUTO_PACKAGE_NAME == clientPackageName -> true
else -> false
override fun onLoadChildren(parentId: String, result: Result<MutableList<BrowserMediaItem>>) {
runBlocking(Dispatchers.IO) {
result.sendResult(
when (parentId) {
MediaId.root -> mutableListOf(
songsBrowserMediaItem,
playlistsBrowserMediaItem,
albumsBrowserMediaItem
)
MediaId.songs -> Database
.songsByPlayTimeDesc()
.first()
.take(30)
.also { lastSongs = it }
.map { it.asBrowserMediaItem }
.toMutableList()
.apply {
if (isNotEmpty()) add(0, shuffleBrowserMediaItem)
}
MediaId.playlists -> Database
.playlistPreviewsByDateAddedDesc()
.first()
.map { it.asBrowserMediaItem }
.toMutableList()
.apply {
add(0, favoritesBrowserMediaItem)
add(1, offlineBrowserMediaItem)
}
MediaId.albums -> Database
.albumsByRowIdDesc()
.first()
.map { it.asBrowserMediaItem }
.toMutableList()
else -> mutableListOf()
}
)
}
}
companion object {
const val ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead"
const val CONTENT_STYLE_BROWSABLE_HINT = "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"
const val CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1
const val MEDIA_ROOT_ID = "VIMUSIC_MEDIA_ROOT_ID"
const val MEDIA_PLAYLISTS_ID = "VIMUSIC_MEDIA_PLAYLISTS_ID"
const val MEDIA_FAVORITES_ID = "VIMUSIC_MEDIA_FAVORITES_ID"
const val MEDIA_SONGS_ID = "VIMUSIC_MEDIA_SONGS_ID"
private fun uriFor(@DrawableRes id: Int) = Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(resources.getResourcePackageName(id))
.appendPath(resources.getResourceTypeName(id))
.appendPath(resources.getResourceEntryName(id))
.build()
private val shuffleBrowserMediaItem
inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder()
.setMediaId(MediaId.shuffle)
.setTitle("Shuffle")
.setIconUri(uriFor(R.drawable.shuffle))
.build(),
BrowserMediaItem.FLAG_PLAYABLE
)
private val songsBrowserMediaItem
inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder()
.setMediaId(MediaId.songs)
.setTitle("Songs")
.setIconUri(uriFor(R.drawable.musical_notes))
.build(),
BrowserMediaItem.FLAG_BROWSABLE
)
private val playlistsBrowserMediaItem
inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder()
.setMediaId(MediaId.playlists)
.setTitle("Playlists")
.setIconUri(uriFor(R.drawable.playlist))
.build(),
BrowserMediaItem.FLAG_BROWSABLE
)
private val albumsBrowserMediaItem
inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder()
.setMediaId(MediaId.albums)
.setTitle("Albums")
.setIconUri(uriFor(R.drawable.disc))
.build(),
BrowserMediaItem.FLAG_BROWSABLE
)
private val favoritesBrowserMediaItem
inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder()
.setMediaId(MediaId.favorites)
.setTitle("Favorites")
.setIconUri(uriFor(R.drawable.heart))
.build(),
BrowserMediaItem.FLAG_PLAYABLE
)
private val offlineBrowserMediaItem
inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder()
.setMediaId(MediaId.offline)
.setTitle("Offline")
.setIconUri(uriFor(R.drawable.airplane))
.build(),
BrowserMediaItem.FLAG_PLAYABLE
)
private val DetailedSong.asBrowserMediaItem
inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder()
.setMediaId(MediaId.forSong(id))
.setTitle(title)
.setSubtitle(artistsText)
.setIconUri(thumbnailUrl?.toUri())
.build(),
BrowserMediaItem.FLAG_PLAYABLE
)
private val PlaylistPreview.asBrowserMediaItem
inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder()
.setMediaId(MediaId.forPlaylist(playlist.id))
.setTitle(playlist.name)
.setSubtitle("$songCount songs")
.setIconUri(uriFor(R.drawable.playlist))
.build(),
BrowserMediaItem.FLAG_PLAYABLE
)
private val Album.asBrowserMediaItem
inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder()
.setMediaId(MediaId.forAlbum(id))
.setTitle(title)
.setSubtitle(authorsText)
.setIconUri(thumbnailUrl?.toUri())
.build(),
BrowserMediaItem.FLAG_PLAYABLE
)
private inner class SessionCallback(private val player: Player, private val cache: Cache) :
MediaSession.Callback() {
override fun onPlay() = player.play()
override fun onPause() = player.pause()
override fun onSkipToPrevious() = player.forceSeekToPrevious()
override fun onSkipToNext() = player.forceSeekToNext()
override fun onSeekTo(pos: Long) = player.seekTo(pos)
override fun onSkipToQueueItem(id: Long) = player.seekToDefaultPosition(id.toInt())
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
val data = mediaId?.split('/') ?: return
coroutineScope.launch {
val mediaItems = when (data.getOrNull(0)) {
MediaId.shuffle -> lastSongs
MediaId.songs -> data
.getOrNull(1)
?.let { songId ->
val index = lastSongs.indexOfFirst { it.id == songId }
if (index != -1) {
val mediaItems = lastSongs.map(DetailedSong::asMediaItem)
withContext(Dispatchers.Main) {
player.forcePlayAtIndex(mediaItems, index)
}
return@launch
}
emptyList()
} ?: emptyList()
MediaId.favorites -> Database
.favorites()
.first()
MediaId.offline -> Database
.songsWithContentLength()
.first()
.filter { song ->
song.contentLength?.let {
cache.isCached(song.id, 0, song.contentLength)
} ?: false
}
MediaId.playlists -> data
.getOrNull(1)
?.toLongOrNull()
?.let { playlistId ->
Database.playlistWithSongs(playlistId).first()?.songs
} ?: emptyList()
MediaId.albums -> data
.getOrNull(1)
?.let { albumId ->
Database.albumSongs(albumId).first()
} ?: emptyList()
else -> emptyList()
}.map(DetailedSong::asMediaItem).shuffled()
withContext(Dispatchers.Main) {
player.forcePlayFromBeginning(mediaItems)
}
}
}
}
}
private object MediaId {
const val root = "root"
const val songs = "songs"
const val playlists = "playlists"
const val albums = "albums"
const val favorites = "favorites"
const val offline = "offline"
const val shuffle = "shuffle"
fun forSong(id: String) = "songs/$id"
fun forPlaylist(id: Long) = "playlists/$id"
fun forAlbum(id: String) = "albums/$id"
}
}

View file

@ -13,14 +13,12 @@ import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
import android.media.MediaDescription
import android.media.MediaMetadata
import android.media.audiofx.AudioEffect
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.text.format.DateUtils
import androidx.compose.runtime.getValue
@ -72,7 +70,6 @@ import it.vfsfitvnm.vimusic.models.Event
import it.vfsfitvnm.vimusic.models.QueuedMediaItem
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.utils.InvincibleService
import it.vfsfitvnm.vimusic.utils.MediaIDHelper
import it.vfsfitvnm.vimusic.utils.RingBuffer
import it.vfsfitvnm.vimusic.utils.TimerJob
import it.vfsfitvnm.vimusic.utils.YouTubeRadio
@ -327,18 +324,18 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
}
// On playlist changed, we refresh the mediaSession queue
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) {
mediaSession.setQueue(player.currentTimeline.mediaItems.mapIndexed { index, it ->
MediaSession.QueueItem(
MediaDescription.Builder()
.setMediaId(it.mediaId)
.setTitle(it.mediaMetadata.title)
.setSubtitle(it.mediaMetadata.artist)
.setIconUri(it.mediaMetadata.artworkUri)
.build(), index.toLong()
)
})
}
// if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) {
// mediaSession.setQueue(player.currentTimeline.mediaItems.mapIndexed { index, it ->
// MediaSession.QueueItem(
// MediaDescription.Builder()
// .setMediaId(it.mediaId)
// .setTitle(it.mediaMetadata.title)
// .setSubtitle(it.mediaMetadata.artist)
// .setIconUri(it.mediaMetadata.artworkUri)
// .build(), index.toLong()
// )
// })
// }
}
private fun maybeRecoverPlaybackError() {
@ -486,10 +483,10 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
MediaMetadata.METADATA_KEY_ALBUM,
player.currentMediaItem?.mediaMetadata?.albumTitle
)
.putBitmap(
MediaMetadata.METADATA_KEY_ALBUM_ART,
if (isShowingThumbnailInLockscreen) bitmapProvider.bitmap else null
)
// .putBitmap(
// MediaMetadata.METADATA_KEY_ALBUM_ART,
// if (isShowingThumbnailInLockscreen) bitmapProvider.bitmap else null
// )
.putLong(MediaMetadata.METADATA_KEY_DURATION, player.duration)
.build().let(mediaSession::setMetadata)
}
@ -555,7 +552,6 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
}
}
override fun notification(): Notification? {
if (player.currentMediaItem == null) return null
@ -780,7 +776,7 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
val cache: Cache
get() = this@PlayerService.cache
val mediaSession: MediaSession
val mediaSession
get() = this@PlayerService.mediaSession
val sleepTimerMillisLeft: StateFlow<Long?>?
@ -862,11 +858,6 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
override fun onSkipToPrevious() = player.forceSeekToPrevious()
override fun onSkipToNext() = player.forceSeekToNext()
override fun onSeekTo(pos: Long) = player.seekTo(pos)
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
player.forcePlayFromBeginning(MediaIDHelper.extractMusicQueueFromMediaId(mediaId))
}
override fun onSkipToQueueItem(id: Long) = player.seekToDefaultPosition(id.toInt())
}
private class NotificationActionReceiver(private val player: Player) : BroadcastReceiver() {

View file

@ -1,77 +0,0 @@
package it.vfsfitvnm.vimusic.utils
import androidx.media3.common.MediaItem
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.enums.MediaIDType
import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
class MediaIDHelper {
companion object {
fun createMediaIdForSong(id: String): String {
return MediaIDType.Song.prefix.plus(id)
}
fun createMediaIdForPlaylist(id: Long): String {
return MediaIDType.Playlist.prefix.plus(id)
}
fun createMediaIdForRandomFavorites(): String {
return MediaIDType.RandomFavorites.prefix
}
fun createMediaIdForRandomSongs(): String {
return MediaIDType.RandomSongs.prefix
}
fun extractMusicQueueFromMediaId(mediaID: String?): List<MediaItem> {
val result = mutableListOf<MediaItem>()
mediaID?.apply {
with(mediaID) {
when {
startsWith(MediaIDType.Song.prefix) -> {
val id = mediaID.removePrefix(MediaIDType.Song.prefix)
val song = runBlocking(Dispatchers.IO) {
Database.songById(id).first()
}
song?.apply {
result.add(song.asMediaItem)
}
}
startsWith(MediaIDType.Playlist.prefix) -> {
val id = mediaID.removePrefix(MediaIDType.Playlist.prefix).toLong()
val playlist = runBlocking(Dispatchers.IO) {
Database.playlistWithSongs(id).first()
}
playlist?.apply {
if (playlist.songs.isNotEmpty()) {
playlist.songs.map { it.asMediaItem }.forEach(result::add)
}
}
}
startsWith(MediaIDType.RandomFavorites.prefix) -> {
val favorites = runBlocking(Dispatchers.IO) {
Database.favorites().first()
}
favorites.map { it.asMediaItem }.forEach(result::add)
result.shuffle()
}
startsWith(MediaIDType.RandomSongs.prefix) -> {
val favorites = runBlocking(Dispatchers.IO) {
Database.songs(SongSortBy.DateAdded, SortOrder.Descending).first()
}
favorites.map { it.asMediaItem }.forEach(result::add)
result.shuffle()
}
}
}
}
return result
}
}
}

View file

@ -4,6 +4,6 @@
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:fillColor="#FFFFFFFF"
android:pathData="M186.62,464H160a16,16 0,0 1,-14.57 -22.6l64.46,-142.25L113.1,297 77.8,339.77C71.07,348.23 65.7,352 52,352H34.08a17.66,17.66 0,0 1,-14.7 -7.06c-2.38,-3.21 -4.72,-8.65 -2.44,-16.41l19.82,-71c0.15,-0.53 0.33,-1.06 0.53,-1.58a0.38,0.38 0,0 0,0 -0.15,14.82 14.82,0 0,1 -0.53,-1.59L16.92,182.76c-2.15,-7.61 0.2,-12.93 2.56,-16.06a16.83,16.83 0,0 1,13.6 -6.7H52c10.23,0 20.16,4.59 26,12l34.57,42.05 97.32,-1.44 -64.44,-142A16,16 0,0 1,160 48h26.91a25,25 0,0 1,19.35 9.8l125.05,152 57.77,-1.52c4.23,-0.23 15.95,-0.31 18.66,-0.31C463,208 496,225.94 496,256c0,9.46 -3.78,27 -29.07,38.16 -14.93,6.6 -34.85,9.94 -59.21,9.94 -2.68,0 -14.37,-0.08 -18.66,-0.31l-57.76,-1.54 -125.36,152A25,25 0,0 1,186.62 464Z"/>
</vector>

View file

@ -4,9 +4,9 @@
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:fillColor="#FFFFFFFF"
android:pathData="M256,176a80,80 0,1 0,80 80A80.09,80.09 0,0 0,256 176ZM256,288a32,32 0,1 1,32 -32A32,32 0,0 1,256 288Z"/>
<path
android:fillColor="#FF000000"
android:fillColor="#FFFFFFFF"
android:pathData="M414.39,97.61A224,224 0,1 0,97.61 414.39,224 224,0 1,0 414.39,97.61ZM256,368A112,112 0,1 1,368 256,112.12 112.12,0 0,1 256,368Z"/>
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M256,176a80,80 0,1 0,80 80A80.09,80.09 0,0 0,256 176ZM256,288a32,32 0,1 1,32 -32A32,32 0,0 1,256 288Z"/>
<path
android:fillColor="#FFFFFFFF"
android:pathData="M414.39,97.61A224,224 0,1 0,97.61 414.39,224 224,0 1,0 414.39,97.61ZM256,368A112,112 0,1 1,368 256,112.12 112.12,0 0,1 256,368Z"/>
</vector>

View file

@ -4,6 +4,6 @@
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:fillColor="#FFFFFFFF"
android:pathData="M256,448a32,32 0,0 1,-18 -5.57c-78.59,-53.35 -112.62,-89.93 -131.39,-112.8 -40,-48.75 -59.15,-98.8 -58.61,-153C48.63,114.52 98.46,64 159.08,64c44.08,0 74.61,24.83 92.39,45.51a6,6 0,0 0,9.06 0C278.31,88.81 308.84,64 352.92,64 413.54,64 463.37,114.52 464,176.64c0.54,54.21 -18.63,104.26 -58.61,153 -18.77,22.87 -52.8,59.45 -131.39,112.8A32,32 0,0 1,256 448Z"/>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M256,448a32,32 0,0 1,-18 -5.57c-78.59,-53.35 -112.62,-89.93 -131.39,-112.8 -40,-48.75 -59.15,-98.8 -58.61,-153C48.63,114.52 98.46,64 159.08,64c44.08,0 74.61,24.83 92.39,45.51a6,6 0,0 0,9.06 0C278.31,88.81 308.84,64 352.92,64 413.54,64 463.37,114.52 464,176.64c0.54,54.21 -18.63,104.26 -58.61,153 -18.77,22.87 -52.8,59.45 -131.39,112.8A32,32 0,0 1,256 448Z"/>
</vector>

View file

@ -4,6 +4,6 @@
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:fillColor="#FFFFFFFF"
android:pathData="M421.84,37.37a25.86,25.86 0,0 0,-22.6 -4.46L199.92,86.49A32.3,32.3 0,0 0,176 118v226c0,6.74 -4.36,12.56 -11.11,14.83l-0.12,0.05 -52,18C92.88,383.53 80,402 80,423.91a55.54,55.54 0,0 0,23.23 45.63A54.78,54.78 0,0 0,135.34 480a55.82,55.82 0,0 0,17.75 -2.93l0.38,-0.13L175.31,469A47.84,47.84 0,0 0,208 423.91v-212c0,-7.29 4.77,-13.21 12.16,-15.07l0.21,-0.06L395,150.14a4,4 0,0 1,5 3.86V295.93c0,6.75 -4.25,12.38 -11.11,14.68l-0.25,0.09 -50.89,18.11A49.09,49.09 0,0 0,304 375.92a55.67,55.67 0,0 0,23.23 45.8,54.63 54.63,0 0,0 49.88,7.35l0.36,-0.12L399.31,421A47.83,47.83 0,0 0,432 375.92V58A25.74,25.74 0,0 0,421.84 37.37Z"/>
</vector>

View file

@ -8,39 +8,39 @@
android:strokeLineJoin="round"
android:strokeWidth="48"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M80.98,112m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeLineJoin="round"
android:strokeWidth="32"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M80.98,224m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeLineJoin="round"
android:strokeWidth="32"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M80.98,336m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeLineJoin="round"
android:strokeWidth="32"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M160.98,312L218.65,312A24,24 0,0 1,242.65 336L242.65,336A24,24 0,0 1,218.65 360L160.98,360A24,24 0,0 1,136.98 336L136.98,336A24,24 0,0 1,160.98 312z"
android:strokeWidth="8.97186"
android:fillColor="#000000"/>
android:fillColor="#FFFFFF"/>
<path
android:pathData="M160.98,200L300.29,200A24,24 0,0 1,324.29 224L324.29,224A24,24 0,0 1,300.29 248L160.98,248A24,24 0,0 1,136.98 224L136.98,224A24,24 0,0 1,160.98 200z"
android:strokeWidth="11.9451"
android:fillColor="#000000"/>
android:fillColor="#FFFFFF"/>
<path
android:fillColor="#FF000000"
android:fillColor="#FFFFFF"
android:pathData="m341.62,486a36.22,36.22 0,0 1,-21.24 -6.92,37.17 37.17,0 0,1 -15.4,-30.1 32.98,32.98 0,0 1,22.4 -31.32l33.38,-11.23a10.66,10.66 0,0 0,7.22 -10.17V231.37a21.07,21.07 0,0 1,15.81 -20.44l71.13,-18.47a14.44,14.44 0,0 1,18.06 13.97v37.9a21.06,21.06 0,0 1,-15.88 20.47l-60.15,15.18a10.66,10.66 0,0 0,-7.97 10.4v158.87a31.64,31.64 0,0 1,-21.51 30.06l-14.09,4.74a36.75,36.75 0,0 1,-11.76 1.94z"
android:strokeWidth="0.656249"/>
</vector>

View file

@ -1,46 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M160.98,112L448.98,112"
android:strokeLineJoin="round"
android:strokeWidth="48"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M80.98,112m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeLineJoin="round"
android:strokeWidth="32"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M80.98,224m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeLineJoin="round"
android:strokeWidth="32"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M80.98,336m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeLineJoin="round"
android:strokeWidth="32"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M160.98,312L218.65,312A24,24 0,0 1,242.65 336L242.65,336A24,24 0,0 1,218.65 360L160.98,360A24,24 0,0 1,136.98 336L136.98,336A24,24 0,0 1,160.98 312z"
android:strokeWidth="8.97186"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M160.98,200L300.29,200A24,24 0,0 1,324.29 224L324.29,224A24,24 0,0 1,300.29 248L160.98,248A24,24 0,0 1,136.98 224L136.98,224A24,24 0,0 1,160.98 200z"
android:strokeWidth="11.9451"
android:fillColor="#FFFFFF"/>
<path
android:fillColor="#FFFFFFFF"
android:pathData="m341.62,486a36.22,36.22 0,0 1,-21.24 -6.92,37.17 37.17,0 0,1 -15.4,-30.1 32.98,32.98 0,0 1,22.4 -31.32l33.38,-11.23a10.66,10.66 0,0 0,7.22 -10.17V231.37a21.07,21.07 0,0 1,15.81 -20.44l71.13,-18.47a14.44,14.44 0,0 1,18.06 13.97v37.9a21.06,21.06 0,0 1,-15.88 20.47l-60.15,15.18a10.66,10.66 0,0 0,-7.97 10.4v158.87a31.64,31.64 0,0 1,-21.51 30.06l-14.09,4.74a36.75,36.75 0,0 1,-11.76 1.94z"
android:strokeWidth="0.656249"/>
</vector>