playback: rework state restore
Rework state restore to be more coherent and in line with the new member layout of the general This primarily involves making the index the primary database attribute in favor of song, with the old song id field becoming a sanity check field.
This commit is contained in:
parent
1e7a439c31
commit
e451bc9859
4 changed files with 202 additions and 194 deletions
|
@ -276,7 +276,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
*/
|
*/
|
||||||
fun savePlaybackState(context: Context, onDone: () -> Unit) {
|
fun savePlaybackState(context: Context, onDone: () -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
playbackManager.saveStateToDatabase(context)
|
playbackManager.saveState(context)
|
||||||
onDone()
|
onDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -293,12 +293,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
playWithUriInternal(intentUri, context)
|
playWithUriInternal(intentUri, context)
|
||||||
// Remove the uri after finishing the calls so that this does not fire again.
|
// Remove the uri after finishing the calls so that this does not fire again.
|
||||||
mIntentUri = null
|
mIntentUri = null
|
||||||
|
|
||||||
// Were not going to be restoring playbackManager after this, so mark it as such.
|
|
||||||
playbackManager.markRestored()
|
|
||||||
} else if (!playbackManager.isInitialized) {
|
} else if (!playbackManager.isInitialized) {
|
||||||
// Otherwise just restore
|
// Otherwise just restore
|
||||||
viewModelScope.launch { playbackManager.restoreFromDatabase(context) }
|
viewModelScope.launch { playbackManager.restoreState(context) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,7 +324,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueueChanged(index: Int, queue: List<Song>) {
|
override fun onQueueChanged(index: Int, queue: List<Song>) {
|
||||||
mSong.value = playbackManager.song
|
|
||||||
mNextUp.value = queue.slice(index.inc() until queue.size)
|
mNextUp.value = queue.slice(index.inc() until queue.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,9 @@ import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import androidx.core.database.getLongOrNull
|
import androidx.core.database.getLongOrNull
|
||||||
import androidx.core.database.sqlite.transaction
|
import androidx.core.database.sqlite.transaction
|
||||||
|
import org.oxycblt.auxio.music.Album
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
@ -34,8 +37,6 @@ import org.oxycblt.auxio.util.requireBackgroundThread
|
||||||
* A SQLite database for managing the persistent playback state and queue. Yes. I know Room exists.
|
* A SQLite database for managing the persistent playback state and queue. Yes. I know Room exists.
|
||||||
* But that would needlessly bloat my app and has crippling bugs.
|
* But that would needlessly bloat my app and has crippling bugs.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*
|
|
||||||
* TODO: Rework to rely on queue indices more and only use specific items as fallbacks
|
|
||||||
*/
|
*/
|
||||||
class PlaybackStateDatabase(context: Context) :
|
class PlaybackStateDatabase(context: Context) :
|
||||||
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
||||||
|
@ -77,10 +78,10 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
private fun constructStateTable(command: StringBuilder): StringBuilder {
|
private fun constructStateTable(command: StringBuilder): StringBuilder {
|
||||||
command
|
command
|
||||||
.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
|
.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
|
||||||
.append("${StateColumns.COLUMN_SONG_HASH} LONG,")
|
.append("${StateColumns.COLUMN_SONG_ID} LONG,")
|
||||||
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
|
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
|
||||||
.append("${StateColumns.COLUMN_PARENT_HASH} LONG,")
|
.append("${StateColumns.COLUMN_PARENT_ID} LONG,")
|
||||||
.append("${StateColumns.COLUMN_QUEUE_INDEX} INTEGER NOT NULL,")
|
.append("${StateColumns.COLUMN_INDEX} INTEGER NOT NULL,")
|
||||||
.append("${StateColumns.COLUMN_PLAYBACK_MODE} INTEGER NOT NULL,")
|
.append("${StateColumns.COLUMN_PLAYBACK_MODE} INTEGER NOT NULL,")
|
||||||
.append("${StateColumns.COLUMN_IS_SHUFFLED} BOOLEAN NOT NULL,")
|
.append("${StateColumns.COLUMN_IS_SHUFFLED} BOOLEAN NOT NULL,")
|
||||||
.append("${StateColumns.COLUMN_REPEAT_MODE} INTEGER NOT NULL)")
|
.append("${StateColumns.COLUMN_REPEAT_MODE} INTEGER NOT NULL)")
|
||||||
|
@ -92,102 +93,73 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
private fun constructQueueTable(command: StringBuilder): StringBuilder {
|
private fun constructQueueTable(command: StringBuilder): StringBuilder {
|
||||||
command
|
command
|
||||||
.append("${QueueColumns.ID} LONG PRIMARY KEY,")
|
.append("${QueueColumns.ID} LONG PRIMARY KEY,")
|
||||||
.append("${QueueColumns.SONG_HASH} INTEGER NOT NULL,")
|
.append("${QueueColumns.SONG_ID} INTEGER NOT NULL,")
|
||||||
.append("${QueueColumns.ALBUM_HASH} INTEGER NOT NULL)")
|
.append("${QueueColumns.ALBUM_ID} INTEGER NOT NULL)")
|
||||||
|
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- INTERFACE FUNCTIONS ---
|
// --- INTERFACE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
fun read(library: MusicStore.Library): SavedState? {
|
||||||
* Read the stored [SavedState] from the database, if there is one.
|
|
||||||
* @param library Required to transform database songs/parents into actual instances
|
|
||||||
* @return The stored [SavedState], null if there isn't one.
|
|
||||||
*/
|
|
||||||
fun readState(library: MusicStore.Library): SavedState? {
|
|
||||||
requireBackgroundThread()
|
requireBackgroundThread()
|
||||||
|
|
||||||
var state: SavedState? = null
|
val rawState = readRawState() ?: return null
|
||||||
|
val queue = readQueue(library)
|
||||||
|
|
||||||
readableDatabase.queryAll(TABLE_NAME_STATE) { cursor ->
|
var actualIndex = rawState.index
|
||||||
if (cursor.count == 0) return@queryAll
|
while (queue.getOrNull(actualIndex)?.id != rawState.songId && actualIndex > -1) {
|
||||||
|
actualIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
val parent =
|
||||||
|
when (rawState.playbackMode) {
|
||||||
|
PlaybackMode.ALL_SONGS -> null
|
||||||
|
PlaybackMode.IN_ALBUM -> library.albums.find { it.id == rawState.parentId }
|
||||||
|
PlaybackMode.IN_ARTIST -> library.artists.find { it.id == rawState.parentId }
|
||||||
|
PlaybackMode.IN_GENRE -> library.genres.find { it.id == rawState.parentId }
|
||||||
|
}
|
||||||
|
|
||||||
|
return SavedState(
|
||||||
|
index = actualIndex,
|
||||||
|
parent = parent,
|
||||||
|
queue = queue,
|
||||||
|
positionMs = rawState.positionMs,
|
||||||
|
repeatMode = rawState.repeatMode,
|
||||||
|
isShuffled = rawState.isShuffled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readRawState(): RawState? {
|
||||||
|
return readableDatabase.queryAll(TABLE_NAME_STATE) { cursor ->
|
||||||
|
if (cursor.count == 0) {
|
||||||
|
return@queryAll null
|
||||||
|
}
|
||||||
|
|
||||||
|
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_INDEX)
|
||||||
|
|
||||||
val songIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_HASH)
|
|
||||||
val posIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_POSITION)
|
val posIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_POSITION)
|
||||||
val parentIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_HASH)
|
val playbackModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PLAYBACK_MODE)
|
||||||
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_QUEUE_INDEX)
|
|
||||||
val modeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PLAYBACK_MODE)
|
|
||||||
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLED)
|
|
||||||
val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_REPEAT_MODE)
|
val repeatModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_REPEAT_MODE)
|
||||||
|
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLED)
|
||||||
|
val songIdIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_ID)
|
||||||
|
val parentIdIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_ID)
|
||||||
|
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
|
|
||||||
val song =
|
RawState(
|
||||||
cursor.getLongOrNull(songIndex)?.let { id -> library.songs.find { it.id == id } }
|
index = cursor.getInt(indexIndex),
|
||||||
|
positionMs = cursor.getLong(posIndex),
|
||||||
val mode = PlaybackMode.fromInt(cursor.getInt(modeIndex)) ?: PlaybackMode.ALL_SONGS
|
repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex))
|
||||||
|
?: RepeatMode.NONE,
|
||||||
val parent =
|
isShuffled = cursor.getInt(shuffleIndex) == 1,
|
||||||
cursor.getLongOrNull(parentIndex)?.let { id ->
|
songId = cursor.getLong(songIdIndex),
|
||||||
when (mode) {
|
parentId = cursor.getLongOrNull(parentIdIndex),
|
||||||
PlaybackMode.IN_GENRE -> library.genres.find { it.id == id }
|
playbackMode = PlaybackMode.fromInt(playbackModeIndex) ?: PlaybackMode.ALL_SONGS)
|
||||||
PlaybackMode.IN_ARTIST -> library.artists.find { it.id == id }
|
|
||||||
PlaybackMode.IN_ALBUM -> library.albums.find { it.id == id }
|
|
||||||
PlaybackMode.ALL_SONGS -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state =
|
|
||||||
SavedState(
|
|
||||||
song = song,
|
|
||||||
positionMs = cursor.getLong(posIndex),
|
|
||||||
parent = parent,
|
|
||||||
queueIndex = cursor.getInt(indexIndex),
|
|
||||||
playbackMode = mode,
|
|
||||||
isShuffled = cursor.getInt(shuffleIndex) == 1,
|
|
||||||
repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex))
|
|
||||||
?: RepeatMode.NONE,
|
|
||||||
)
|
|
||||||
|
|
||||||
logD("Successfully read playback state: $state")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear the previously written [SavedState] and write a new one. */
|
private fun readQueue(library: MusicStore.Library): MutableList<Song> {
|
||||||
fun writeState(state: SavedState) {
|
|
||||||
requireBackgroundThread()
|
|
||||||
|
|
||||||
writableDatabase.transaction {
|
|
||||||
delete(TABLE_NAME_STATE, null, null)
|
|
||||||
|
|
||||||
this@PlaybackStateDatabase.logD("Wiped state db")
|
|
||||||
|
|
||||||
val stateData =
|
|
||||||
ContentValues(10).apply {
|
|
||||||
put(StateColumns.COLUMN_ID, 0)
|
|
||||||
put(StateColumns.COLUMN_SONG_HASH, state.song?.id)
|
|
||||||
put(StateColumns.COLUMN_POSITION, state.positionMs)
|
|
||||||
put(StateColumns.COLUMN_PARENT_HASH, state.parent?.id)
|
|
||||||
put(StateColumns.COLUMN_QUEUE_INDEX, state.queueIndex)
|
|
||||||
put(StateColumns.COLUMN_PLAYBACK_MODE, state.playbackMode.intCode)
|
|
||||||
put(StateColumns.COLUMN_IS_SHUFFLED, state.isShuffled)
|
|
||||||
put(StateColumns.COLUMN_REPEAT_MODE, state.repeatMode.intCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
insert(TABLE_NAME_STATE, null, stateData)
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Wrote state to database")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a list of queue items from this database.
|
|
||||||
* @param musicStore Required to transform database songs into actual song instances
|
|
||||||
*/
|
|
||||||
fun readQueue(library: MusicStore.Library): MutableList<Song> {
|
|
||||||
requireBackgroundThread()
|
requireBackgroundThread()
|
||||||
|
|
||||||
val queue = mutableListOf<Song>()
|
val queue = mutableListOf<Song>()
|
||||||
|
@ -195,8 +167,8 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
|
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
|
||||||
if (cursor.count == 0) return@queryAll
|
if (cursor.count == 0) return@queryAll
|
||||||
|
|
||||||
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_HASH)
|
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_ID)
|
||||||
val albumIndex = cursor.getColumnIndexOrThrow(QueueColumns.ALBUM_HASH)
|
val albumIndex = cursor.getColumnIndexOrThrow(QueueColumns.ALBUM_ID)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
library.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))?.let {
|
library.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))?.let {
|
||||||
|
@ -211,67 +183,126 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
return queue
|
return queue
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Write a queue to the database. */
|
/** Clear the previously written [SavedState] and write a new one. */
|
||||||
fun writeQueue(queue: MutableList<Song>) {
|
fun write(state: SavedState) {
|
||||||
requireBackgroundThread()
|
requireBackgroundThread()
|
||||||
|
|
||||||
|
val song = state.queue.getOrNull(state.index)
|
||||||
|
|
||||||
|
if (song != null) {
|
||||||
|
val rawState =
|
||||||
|
RawState(
|
||||||
|
index = state.index,
|
||||||
|
positionMs = state.positionMs,
|
||||||
|
repeatMode = state.repeatMode,
|
||||||
|
isShuffled = state.isShuffled,
|
||||||
|
songId = song.id,
|
||||||
|
parentId = state.parent?.id,
|
||||||
|
playbackMode =
|
||||||
|
when (state.parent) {
|
||||||
|
null -> PlaybackMode.ALL_SONGS
|
||||||
|
is Album -> PlaybackMode.IN_ALBUM
|
||||||
|
is Artist -> PlaybackMode.IN_ARTIST
|
||||||
|
is Genre -> PlaybackMode.IN_GENRE
|
||||||
|
})
|
||||||
|
|
||||||
|
writeRawState(rawState)
|
||||||
|
writeQueue(state.queue)
|
||||||
|
} else {
|
||||||
|
writeRawState(null)
|
||||||
|
writeQueue(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
logD("Wrote state to database")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeRawState(rawState: RawState?) {
|
||||||
|
writableDatabase.transaction {
|
||||||
|
delete(TABLE_NAME_STATE, null, null)
|
||||||
|
|
||||||
|
if (rawState != null) {
|
||||||
|
val stateData =
|
||||||
|
ContentValues(10).apply {
|
||||||
|
put(StateColumns.COLUMN_ID, 0)
|
||||||
|
put(StateColumns.COLUMN_SONG_ID, rawState.songId)
|
||||||
|
put(StateColumns.COLUMN_POSITION, rawState.positionMs)
|
||||||
|
put(StateColumns.COLUMN_PARENT_ID, rawState.parentId)
|
||||||
|
put(StateColumns.COLUMN_INDEX, rawState.index)
|
||||||
|
put(StateColumns.COLUMN_PLAYBACK_MODE, rawState.playbackMode.intCode)
|
||||||
|
put(StateColumns.COLUMN_IS_SHUFFLED, rawState.isShuffled)
|
||||||
|
put(StateColumns.COLUMN_REPEAT_MODE, rawState.repeatMode.intCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
insert(TABLE_NAME_STATE, null, stateData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write a queue to the database. */
|
||||||
|
private fun writeQueue(queue: List<Song>?) {
|
||||||
val database = writableDatabase
|
val database = writableDatabase
|
||||||
database.transaction { delete(TABLE_NAME_QUEUE, null, null) }
|
database.transaction { delete(TABLE_NAME_QUEUE, null, null) }
|
||||||
|
|
||||||
logD("Wiped queue db")
|
logD("Wiped queue db")
|
||||||
|
|
||||||
writeQueueBatch(queue, queue.size)
|
if (queue != null) {
|
||||||
}
|
val idStart = queue.size
|
||||||
|
logD("Beginning queue write [start: $idStart]")
|
||||||
|
var position = 0
|
||||||
|
|
||||||
private fun writeQueueBatch(queue: List<Song>, idStart: Int) {
|
while (position < queue.size) {
|
||||||
logD("Beginning queue write [start: $idStart]")
|
var i = position
|
||||||
|
|
||||||
val database = writableDatabase
|
database.transaction {
|
||||||
var position = 0
|
while (i < queue.size) {
|
||||||
|
val song = queue[i]
|
||||||
|
i++
|
||||||
|
|
||||||
while (position < queue.size) {
|
val itemData =
|
||||||
var i = position
|
ContentValues(4).apply {
|
||||||
|
put(QueueColumns.ID, idStart + i)
|
||||||
|
put(QueueColumns.SONG_ID, song.id)
|
||||||
|
put(QueueColumns.ALBUM_ID, song.album.id)
|
||||||
|
}
|
||||||
|
|
||||||
database.transaction {
|
insert(TABLE_NAME_QUEUE, null, itemData)
|
||||||
while (i < queue.size) {
|
}
|
||||||
val song = queue[i]
|
|
||||||
i++
|
|
||||||
|
|
||||||
val itemData =
|
|
||||||
ContentValues(4).apply {
|
|
||||||
put(QueueColumns.ID, idStart + i)
|
|
||||||
put(QueueColumns.SONG_HASH, song.id)
|
|
||||||
put(QueueColumns.ALBUM_HASH, song.album.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
insert(TABLE_NAME_QUEUE, null, itemData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the position at the end, if an insert failed at any point, then
|
||||||
|
// the next iteration should skip it.
|
||||||
|
position = i
|
||||||
|
|
||||||
|
logD("Wrote batch of songs. Position is now at $position")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the position at the end, if an insert failed at any point, then
|
|
||||||
// the next iteration should skip it.
|
|
||||||
position = i
|
|
||||||
|
|
||||||
logD("Wrote batch of songs. Position is now at $position")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SavedState(
|
data class SavedState(
|
||||||
val song: Song?,
|
val index: Int,
|
||||||
val positionMs: Long,
|
val queue: List<Song>,
|
||||||
val parent: MusicParent?,
|
val parent: MusicParent?,
|
||||||
val queueIndex: Int,
|
val positionMs: Long,
|
||||||
val playbackMode: PlaybackMode,
|
|
||||||
val isShuffled: Boolean,
|
|
||||||
val repeatMode: RepeatMode,
|
val repeatMode: RepeatMode,
|
||||||
|
val isShuffled: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class RawState(
|
||||||
|
val index: Int,
|
||||||
|
val positionMs: Long,
|
||||||
|
val repeatMode: RepeatMode,
|
||||||
|
val isShuffled: Boolean,
|
||||||
|
val songId: Long,
|
||||||
|
val parentId: Long?,
|
||||||
|
val playbackMode: PlaybackMode
|
||||||
)
|
)
|
||||||
|
|
||||||
private object StateColumns {
|
private object StateColumns {
|
||||||
const val COLUMN_ID = "id"
|
const val COLUMN_ID = "id"
|
||||||
const val COLUMN_SONG_HASH = "song"
|
const val COLUMN_SONG_ID = "song"
|
||||||
const val COLUMN_POSITION = "position"
|
const val COLUMN_POSITION = "position"
|
||||||
const val COLUMN_PARENT_HASH = "parent"
|
const val COLUMN_PARENT_ID = "parent"
|
||||||
const val COLUMN_QUEUE_INDEX = "queue_index"
|
const val COLUMN_INDEX = "queue_index"
|
||||||
const val COLUMN_PLAYBACK_MODE = "playback_mode"
|
const val COLUMN_PLAYBACK_MODE = "playback_mode"
|
||||||
const val COLUMN_IS_SHUFFLED = "is_shuffling"
|
const val COLUMN_IS_SHUFFLED = "is_shuffling"
|
||||||
const val COLUMN_REPEAT_MODE = "loop_mode"
|
const val COLUMN_REPEAT_MODE = "loop_mode"
|
||||||
|
@ -279,8 +310,8 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
|
|
||||||
private object QueueColumns {
|
private object QueueColumns {
|
||||||
const val ID = "id"
|
const val ID = "id"
|
||||||
const val SONG_HASH = "song"
|
const val SONG_ID = "song"
|
||||||
const val ALBUM_HASH = "album"
|
const val ALBUM_ID = "album"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -317,20 +317,50 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rework these methods eventually
|
// --- PERSISTENCE FUNCTIONS ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the state from the database
|
||||||
|
* @param context [Context] required.
|
||||||
|
*/
|
||||||
|
suspend fun restoreState(context: Context) {
|
||||||
|
val library = musicStore.library ?: return
|
||||||
|
val start: Long
|
||||||
|
val database = PlaybackStateDatabase.getInstance(context)
|
||||||
|
val state: PlaybackStateDatabase.SavedState?
|
||||||
|
|
||||||
|
logD("Getting state from DB")
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
start = System.currentTimeMillis()
|
||||||
|
state = database.read(library)
|
||||||
|
}
|
||||||
|
|
||||||
|
logD("State read completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||||
|
|
||||||
|
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
|
||||||
|
|
||||||
|
if (state != null) {
|
||||||
|
index = state.index
|
||||||
|
parent = state.parent
|
||||||
|
mutableQueue = state.queue.toMutableList()
|
||||||
|
repeatMode = state.repeatMode
|
||||||
|
isShuffled = state.isShuffled
|
||||||
|
|
||||||
|
notifyNewPlayback()
|
||||||
|
seekTo(state.positionMs)
|
||||||
|
notifyRepeatModeChanged()
|
||||||
|
notifyShuffledChanged()
|
||||||
|
}
|
||||||
|
|
||||||
/** Mark this instance as restored. */
|
|
||||||
fun markRestored() {
|
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PERSISTENCE FUNCTIONS ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the current state to the database.
|
* Save the current state to the database.
|
||||||
* @param context [Context] required
|
* @param context [Context] required
|
||||||
*/
|
*/
|
||||||
suspend fun saveStateToDatabase(context: Context) {
|
suspend fun saveState(context: Context) {
|
||||||
logD("Saving state to DB")
|
logD("Saving state to DB")
|
||||||
|
|
||||||
// Pack the entire state and save it to the database.
|
// Pack the entire state and save it to the database.
|
||||||
|
@ -338,69 +368,20 @@ class PlaybackStateManager private constructor() {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val database = PlaybackStateDatabase.getInstance(context)
|
val database = PlaybackStateDatabase.getInstance(context)
|
||||||
|
|
||||||
val playbackMode =
|
database.write(
|
||||||
when (parent) {
|
|
||||||
is Album -> PlaybackMode.IN_ALBUM
|
|
||||||
is Artist -> PlaybackMode.IN_ARTIST
|
|
||||||
is Genre -> PlaybackMode.IN_GENRE
|
|
||||||
null -> PlaybackMode.ALL_SONGS
|
|
||||||
}
|
|
||||||
|
|
||||||
database.writeState(
|
|
||||||
PlaybackStateDatabase.SavedState(
|
PlaybackStateDatabase.SavedState(
|
||||||
song,
|
index = index,
|
||||||
positionMs,
|
parent = parent,
|
||||||
parent,
|
queue = mutableQueue,
|
||||||
index,
|
positionMs = positionMs,
|
||||||
playbackMode,
|
isShuffled = isShuffled,
|
||||||
isShuffled,
|
repeatMode = repeatMode))
|
||||||
repeatMode,
|
|
||||||
))
|
|
||||||
|
|
||||||
database.writeQueue(mutableQueue)
|
|
||||||
|
|
||||||
this@PlaybackStateManager.logD(
|
this@PlaybackStateManager.logD(
|
||||||
"State save completed successfully in ${System.currentTimeMillis() - start}ms")
|
"State save completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
isInitialized = true
|
||||||
* Restore the state from the database
|
|
||||||
* @param context [Context] required.
|
|
||||||
*/
|
|
||||||
suspend fun restoreFromDatabase(context: Context) {
|
|
||||||
logD("Getting state from DB")
|
|
||||||
|
|
||||||
val library = musicStore.library ?: return
|
|
||||||
val start: Long
|
|
||||||
val playbackState: PlaybackStateDatabase.SavedState?
|
|
||||||
val queue: MutableList<Song>
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
start = System.currentTimeMillis()
|
|
||||||
val database = PlaybackStateDatabase.getInstance(context)
|
|
||||||
playbackState = database.readState(library)
|
|
||||||
queue = database.readQueue(library)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
|
|
||||||
|
|
||||||
if (playbackState != null) {
|
|
||||||
parent = playbackState.parent
|
|
||||||
mutableQueue = queue
|
|
||||||
index = playbackState.queueIndex
|
|
||||||
repeatMode = playbackState.repeatMode
|
|
||||||
isShuffled = playbackState.isShuffled
|
|
||||||
|
|
||||||
notifyNewPlayback()
|
|
||||||
seekTo(playbackState.positionMs)
|
|
||||||
notifyRepeatModeChanged()
|
|
||||||
notifyShuffledChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("State load completed successfully in ${System.currentTimeMillis() - start}ms")
|
|
||||||
|
|
||||||
markRestored()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
|
@ -387,7 +387,7 @@ class PlaybackService :
|
||||||
private fun stopAndSave() {
|
private fun stopAndSave() {
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
isForeground = false
|
isForeground = false
|
||||||
saveScope.launch { playbackManager.saveStateToDatabase(this@PlaybackService) }
|
saveScope.launch { playbackManager.saveState(this@PlaybackService) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A [BroadcastReceiver] for receiving general playback events from the system. */
|
/** A [BroadcastReceiver] for receiving general playback events from the system. */
|
||||||
|
|
Loading…
Reference in a new issue