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.SongPlaylistMap
import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@Dao @Dao
interface Database { interface Database {
@ -127,10 +126,6 @@ interface Database {
@Query("SELECT * FROM Song WHERE id = :id") @Query("SELECT * FROM Song WHERE id = :id")
fun song(id: String): Flow<Song?> 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") @Query("SELECT likedAt FROM Song WHERE id = :songId")
fun likedAt(songId: String): Flow<Long?> fun likedAt(songId: String): Flow<Long?>
@ -238,28 +233,44 @@ interface Database {
@Transaction @Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY name ASC") @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 @Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY ROWID ASC") @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 @Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY songCount ASC") @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( fun playlistPreviews(
sortBy: PlaylistSortBy, sortBy: PlaylistSortBy,
sortOrder: SortOrder sortOrder: SortOrder
): Flow<List<PlaylistPreview>> { ): Flow<List<PlaylistPreview>> {
return when (sortBy) { return when (sortBy) {
PlaylistSortBy.Name -> playlistPreviewsByName() PlaylistSortBy.Name -> when (sortOrder) {
PlaylistSortBy.DateAdded -> playlistPreviewsByDateAdded() SortOrder.Ascending -> playlistPreviewsByNameAsc()
PlaylistSortBy.SongCount -> playlistPreviewsByDateSongCount() SortOrder.Descending -> playlistPreviewsByNameDesc()
}.map { }
when (sortOrder) { PlaylistSortBy.SongCount -> when (sortOrder) {
SortOrder.Ascending -> it SortOrder.Ascending -> playlistPreviewsByDateSongCountAsc()
SortOrder.Descending -> it.reversed() 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 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.ComponentName
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.media.MediaDescription import android.media.session.MediaSession
import android.media.browse.MediaBrowser.MediaItem
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.os.Process import android.os.Process
import android.service.media.MediaBrowserService 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.Database
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.utils.MediaIDHelper 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.Dispatchers
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking 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 private var bound = false
var isBound = false
override fun onCreate() { override fun onDestroy() {
super.onCreate() if (bound) {
val intent = Intent(this, PlayerService::class.java) unbindService(this)
bindService(intent, playerConnection, Context.BIND_AUTO_CREATE)
} }
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( override fun onGetRoot(
clientPackageName: String, clientPackageName: String,
clientUid: Int, clientUid: Int,
rootHints: Bundle? rootHints: Bundle?
): BrowserRoot? { ): BrowserRoot? {
if (!isCallerAllowed(clientPackageName, clientUid)) { return if (clientUid == Process.myUid()
return null || clientUid == Process.SYSTEM_UID
|| clientPackageName == "com.google.android.projection.gearhead"
) {
bindService(intent<PlayerService>(), this, Context.BIND_AUTO_CREATE)
BrowserRoot(
MediaId.root,
bundleOf("android.media.browse.CONTENT_STYLE_BROWSABLE_HINT" to 1)
)
} else {
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( override fun onLoadChildren(parentId: String, result: Result<MutableList<BrowserMediaItem>>) {
parentId: String, runBlocking(Dispatchers.IO) {
result: Result<MutableList<MediaItem>> result.sendResult(
) {
when (parentId) { when (parentId) {
MEDIA_ROOT_ID -> result.sendResult(createMenuMediaItem()) MediaId.root -> mutableListOf(
MEDIA_PLAYLISTS_ID -> result.sendResult(createPlaylistsMediaItem()) songsBrowserMediaItem,
MEDIA_FAVORITES_ID -> result.sendResult(createFavoritesMediaItem()) playlistsBrowserMediaItem,
MEDIA_SONGS_ID -> result.sendResult(createSongsMediaItem()) 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()
}
)
} }
} }
private fun createFavoritesMediaItem(): MutableList<MediaItem> { private fun uriFor(@DrawableRes id: Int) = Uri.Builder()
val favorites = runBlocking(Dispatchers.IO) { .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
Database.favorites().first() .authority(resources.getResourcePackageName(id))
}.map { entry -> .appendPath(resources.getResourceTypeName(id))
MediaItem( .appendPath(resources.getResourceEntryName(id))
MediaDescription.Builder() .build()
.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> { private val shuffleBrowserMediaItem
val songs = runBlocking(Dispatchers.IO) { inline get() = BrowserMediaItem(
Database.songs(SongSortBy.DateAdded, SortOrder.Descending).first() BrowserMediaDescription.Builder()
}.map { entry -> .setMediaId(MediaId.shuffle)
MediaItem( .setTitle("Shuffle")
MediaDescription.Builder() .setIconUri(uriFor(R.drawable.shuffle))
.setMediaId(MediaIDHelper.createMediaIdForSong(entry.id)) .build(),
.setTitle(entry.title) BrowserMediaItem.FLAG_PLAYABLE
.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> { private val songsBrowserMediaItem
return runBlocking(Dispatchers.IO) { inline get() = BrowserMediaItem(
Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending).first() BrowserMediaDescription.Builder()
}.map { entry -> .setMediaId(MediaId.songs)
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") .setTitle("Songs")
.setIconUri( .setIconUri(uriFor(R.drawable.musical_notes))
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/disc_white") .build(),
BrowserMediaItem.FLAG_BROWSABLE
) )
.build(), MediaItem.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
} }
private val playerConnection = object : ServiceConnection { emptyList()
override fun onServiceConnected( } ?: emptyList()
className: ComponentName,
service: IBinder MediaId.favorites -> Database
) { .favorites()
playerServiceBinder = service as PlayerService.Binder .first()
isBound = true
sessionToken = playerServiceBinder?.mediaSession?.sessionToken MediaId.offline -> Database
.songsWithContentLength()
.first()
.filter { song ->
song.contentLength?.let {
cache.isCached(song.id, 0, song.contentLength)
} ?: false
} }
override fun onServiceDisconnected(name: ComponentName) { MediaId.playlists -> data
isBound = false .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 fun isCallerAllowed( private object MediaId {
clientPackageName: String, const val root = "root"
clientUid: Int const val songs = "songs"
): Boolean { const val playlists = "playlists"
return when { const val albums = "albums"
clientUid == Process.myUid() -> true
clientUid == Process.SYSTEM_UID -> true
ANDROID_AUTO_PACKAGE_NAME == clientPackageName -> true
else -> false
}
}
companion object { const val favorites = "favorites"
const val ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead" const val offline = "offline"
const val CONTENT_STYLE_BROWSABLE_HINT = "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT" const val shuffle = "shuffle"
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"
}
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.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.media.MediaDescription
import android.media.MediaMetadata import android.media.MediaMetadata
import android.media.audiofx.AudioEffect import android.media.audiofx.AudioEffect
import android.media.session.MediaSession import android.media.session.MediaSession
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.compose.runtime.getValue 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.models.QueuedMediaItem
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.utils.InvincibleService import it.vfsfitvnm.vimusic.utils.InvincibleService
import it.vfsfitvnm.vimusic.utils.MediaIDHelper
import it.vfsfitvnm.vimusic.utils.RingBuffer import it.vfsfitvnm.vimusic.utils.RingBuffer
import it.vfsfitvnm.vimusic.utils.TimerJob import it.vfsfitvnm.vimusic.utils.TimerJob
import it.vfsfitvnm.vimusic.utils.YouTubeRadio import it.vfsfitvnm.vimusic.utils.YouTubeRadio
@ -327,18 +324,18 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
} }
// On playlist changed, we refresh the mediaSession queue // On playlist changed, we refresh the mediaSession queue
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) { // if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) {
mediaSession.setQueue(player.currentTimeline.mediaItems.mapIndexed { index, it -> // mediaSession.setQueue(player.currentTimeline.mediaItems.mapIndexed { index, it ->
MediaSession.QueueItem( // MediaSession.QueueItem(
MediaDescription.Builder() // MediaDescription.Builder()
.setMediaId(it.mediaId) // .setMediaId(it.mediaId)
.setTitle(it.mediaMetadata.title) // .setTitle(it.mediaMetadata.title)
.setSubtitle(it.mediaMetadata.artist) // .setSubtitle(it.mediaMetadata.artist)
.setIconUri(it.mediaMetadata.artworkUri) // .setIconUri(it.mediaMetadata.artworkUri)
.build(), index.toLong() // .build(), index.toLong()
) // )
}) // })
} // }
} }
private fun maybeRecoverPlaybackError() { private fun maybeRecoverPlaybackError() {
@ -486,10 +483,10 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
MediaMetadata.METADATA_KEY_ALBUM, MediaMetadata.METADATA_KEY_ALBUM,
player.currentMediaItem?.mediaMetadata?.albumTitle player.currentMediaItem?.mediaMetadata?.albumTitle
) )
.putBitmap( // .putBitmap(
MediaMetadata.METADATA_KEY_ALBUM_ART, // MediaMetadata.METADATA_KEY_ALBUM_ART,
if (isShowingThumbnailInLockscreen) bitmapProvider.bitmap else null // if (isShowingThumbnailInLockscreen) bitmapProvider.bitmap else null
) // )
.putLong(MediaMetadata.METADATA_KEY_DURATION, player.duration) .putLong(MediaMetadata.METADATA_KEY_DURATION, player.duration)
.build().let(mediaSession::setMetadata) .build().let(mediaSession::setMetadata)
} }
@ -555,7 +552,6 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
} }
} }
override fun notification(): Notification? { override fun notification(): Notification? {
if (player.currentMediaItem == null) return null if (player.currentMediaItem == null) return null
@ -780,7 +776,7 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
val cache: Cache val cache: Cache
get() = this@PlayerService.cache get() = this@PlayerService.cache
val mediaSession: MediaSession val mediaSession
get() = this@PlayerService.mediaSession get() = this@PlayerService.mediaSession
val sleepTimerMillisLeft: StateFlow<Long?>? val sleepTimerMillisLeft: StateFlow<Long?>?
@ -862,11 +858,6 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
override fun onSkipToPrevious() = player.forceSeekToPrevious() override fun onSkipToPrevious() = player.forceSeekToPrevious()
override fun onSkipToNext() = player.forceSeekToNext() override fun onSkipToNext() = player.forceSeekToNext()
override fun onSeekTo(pos: Long) = player.seekTo(pos) 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() { 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:viewportWidth="512"
android:viewportHeight="512"> android:viewportHeight="512">
<path <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"/> 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> </vector>

View file

@ -4,9 +4,9 @@
android:viewportWidth="512" android:viewportWidth="512"
android:viewportHeight="512"> android:viewportHeight="512">
<path <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"/> 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 <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"/> 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> </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:viewportWidth="512"
android:viewportHeight="512"> android:viewportHeight="512">
<path <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"/> 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> </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:viewportWidth="512"
android:viewportHeight="512"> android:viewportHeight="512">
<path <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"/> 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> </vector>

View file

@ -8,39 +8,39 @@
android:strokeLineJoin="round" android:strokeLineJoin="round"
android:strokeWidth="48" android:strokeWidth="48"
android:fillColor="#00000000" android:fillColor="#00000000"
android:strokeColor="#000000" android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/> android:strokeLineCap="round"/>
<path <path
android:pathData="M80.98,112m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" android:pathData="M80.98,112m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeLineJoin="round" android:strokeLineJoin="round"
android:strokeWidth="32" android:strokeWidth="32"
android:fillColor="#00000000" android:fillColor="#00000000"
android:strokeColor="#000000" android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/> android:strokeLineCap="round"/>
<path <path
android:pathData="M80.98,224m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" android:pathData="M80.98,224m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeLineJoin="round" android:strokeLineJoin="round"
android:strokeWidth="32" android:strokeWidth="32"
android:fillColor="#00000000" android:fillColor="#00000000"
android:strokeColor="#000000" android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/> android:strokeLineCap="round"/>
<path <path
android:pathData="M80.98,336m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" android:pathData="M80.98,336m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeLineJoin="round" android:strokeLineJoin="round"
android:strokeWidth="32" android:strokeWidth="32"
android:fillColor="#00000000" android:fillColor="#00000000"
android:strokeColor="#000000" android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/> android:strokeLineCap="round"/>
<path <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: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:strokeWidth="8.97186"
android:fillColor="#000000"/> android:fillColor="#FFFFFF"/>
<path <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: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:strokeWidth="11.9451"
android:fillColor="#000000"/> android:fillColor="#FFFFFF"/>
<path <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: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"/> android:strokeWidth="0.656249"/>
</vector> </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>