Add player state persistence

This commit is contained in:
vfsfitvnm
2022-06-26 17:07:03 +02:00
parent c336a274d8
commit 8f5bc5e90e
6 changed files with 474 additions and 3 deletions

View File

@@ -2,6 +2,8 @@ package it.vfsfitvnm.vimusic
import android.content.Context
import android.database.Cursor
import android.os.Parcel
import androidx.media3.common.MediaItem
import androidx.room.*
import androidx.room.migration.AutoMigrationSpec
import it.vfsfitvnm.vimusic.models.*
@@ -115,6 +117,15 @@ interface Database {
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM Info JOIN SongWithAuthors ON Info.id = SongWithAuthors.authorInfoId JOIN Song ON SongWithAuthors.songId = Song.id WHERE browseId = :artistId ORDER BY Song.ROWID DESC")
fun artistSongs(artistId: String): Flow<List<SongWithInfo>>
@Insert(onConflict = OnConflictStrategy.ABORT)
fun insertQueue(queuedMediaItems: List<QueuedMediaItem>)
@Query("SELECT * FROM QueuedMediaItem")
fun queue(): List<QueuedMediaItem>
@Query("DELETE FROM QueuedMediaItem")
fun clearQueue()
}
@androidx.room.Database(
@@ -125,19 +136,22 @@ interface Database {
Info::class,
SongWithAuthors::class,
SearchQuery::class,
QueuedMediaItem::class,
],
views = [
SortedSongInPlaylist::class
],
version = 5,
version = 6,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4, spec = DatabaseInitializer.From3To4Migration::class),
AutoMigration(from = 4, to = 5),
AutoMigration(from = 5, to = 6),
],
)
@TypeConverters(Converters::class)
abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
abstract val database: Database
@@ -145,7 +159,7 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
lateinit var Instance: DatabaseInitializer
context(Context)
operator fun invoke() {
operator fun invoke() {
if (!::Instance.isInitialized) {
Instance = Room
.databaseBuilder(this@Context, DatabaseInitializer::class.java, "data.db")
@@ -158,6 +172,38 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
class From3To4Migration : AutoMigrationSpec
}
@TypeConverters
object Converters {
@TypeConverter
fun mediaItemFromByteArray(value: ByteArray?): MediaItem? {
return value?.let { byteArray ->
runCatching {
val parcel = Parcel.obtain()
parcel.unmarshall(byteArray, 0, byteArray.size)
parcel.setDataPosition(0)
val bundle = parcel.readBundle(MediaItem::class.java.classLoader)
parcel.recycle()
bundle?.let(MediaItem.CREATOR::fromBundle)
}.getOrNull()
}
}
@TypeConverter
fun mediaItemToByteArray(mediaItem: MediaItem?): ByteArray? {
return mediaItem?.toBundle()?.let { persistableBundle ->
val parcel = Parcel.obtain()
parcel.writeBundle(persistableBundle)
val bytes = parcel.marshall()
parcel.recycle()
bytes
}
}
}
val Database.internal: RoomDatabase
get() = DatabaseInitializer.Instance

View File

@@ -0,0 +1,14 @@
package it.vfsfitvnm.vimusic.models
import androidx.media3.common.MediaItem
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
class QueuedMediaItem(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) val mediaItem: MediaItem,
var position: Long?
)

View File

@@ -41,6 +41,7 @@ import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.MainActivity
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.internal
import it.vfsfitvnm.vimusic.models.QueuedMediaItem
import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
@@ -139,7 +140,35 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
player.addListener(this)
player.addAnalyticsListener(PlaybackStatsListener(false, this))
mediaSession = MediaSessionCompat(this, "PlayerService")
if (preferences.persistentQueue) {
coroutineScope.launch(Dispatchers.IO) {
val queuedSong = Database.queue()
Database.clearQueue()
if (queuedSong.isEmpty()) return@launch
val index = queuedSong.indexOfFirst { it.position != null }.coerceAtLeast(0)
withContext(Dispatchers.Main) {
player.setMediaItems(
queuedSong
.map(QueuedMediaItem::mediaItem)
.map { mediaItem ->
mediaItem.buildUpon()
.setUri(mediaItem.mediaId)
.setCustomCacheKey(mediaItem.mediaId)
.build()
},
true
)
player.seekTo(index, queuedSong[index].position ?: 0)
player.playWhenReady = false
player.prepare()
}
}
}
mediaSession = MediaSessionCompat(baseContext, "PlayerService")
mediaSession.setCallback(SessionCallback(player))
mediaSession.setPlaybackState(stateBuilder.build())
mediaSession.isActive = true
@@ -161,6 +190,24 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
}
override fun onDestroy() {
if (preferences.persistentQueue) {
val mediaItems = player.currentTimeline.mediaItems
val mediaItemIndex = player.currentMediaItemIndex
val mediaItemPosition = player.currentPosition
Database.internal.queryExecutor.execute {
Database.clearQueue()
Database.insertQueue(
mediaItems.mapIndexed { index, mediaItem ->
QueuedMediaItem(
mediaItem = mediaItem,
position = if (index == mediaItemIndex) mediaItemPosition else null
)
}
)
}
}
player.removeListener(this)
player.stop()
player.release()

View File

@@ -239,6 +239,15 @@ fun PlayerSettingsScreen() {
}
)
SwitchSettingEntry(
title = "Persistent queue",
text = "Save and restore playing songs",
isChecked = preferences.persistentQueue,
onCheckedChange = {
preferences.persistentQueue = it
}
)
SettingsEntry(
title = "Equalizer",
text = "Interact with the system equalizer",

View File

@@ -26,6 +26,7 @@ class Preferences(holder: SharedPreferences) : SharedPreferences by holder {
var exoPlayerDiskCacheMaxSizeBytes by preference("exoPlayerDiskCacheMaxSizeBytes", 512L * 1024 * 1024)
var skipSilence by preference("skipSilence", false)
var volumeNormalization by preference("volumeNormalization", true)
var persistentQueue by preference("persistentQueue", false)
}
val Context.preferences: Preferences