Complete android auto support (#47)
This commit is contained in:
parent
6fb8e41a04
commit
270986215c
13 changed files with 329 additions and 386 deletions
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue