Fix persistence bugs
Fix some edge cases with the persistent PlaybackState restoration.
This commit is contained in:
parent
6d809f4303
commit
212ffbf2c7
7 changed files with 117 additions and 62 deletions
|
@ -7,10 +7,10 @@ import androidx.room.PrimaryKey
|
||||||
@Entity(tableName = "playback_state_table")
|
@Entity(tableName = "playback_state_table")
|
||||||
data class PlaybackState(
|
data class PlaybackState(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
var id: Long = 0L,
|
var id: Long = Long.MIN_VALUE,
|
||||||
|
|
||||||
@ColumnInfo(name = "song_id")
|
@ColumnInfo(name = "song_id")
|
||||||
val songId: Long = -1L,
|
val songId: Long = Long.MIN_VALUE,
|
||||||
|
|
||||||
@ColumnInfo(name = "position")
|
@ColumnInfo(name = "position")
|
||||||
val position: Long,
|
val position: Long,
|
||||||
|
|
|
@ -3,16 +3,12 @@ package org.oxycblt.auxio.database
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Update
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface PlaybackStateDAO {
|
interface PlaybackStateDAO {
|
||||||
@Insert
|
@Insert
|
||||||
fun insert(playbackState: PlaybackState)
|
fun insert(playbackState: PlaybackState)
|
||||||
|
|
||||||
@Update
|
|
||||||
fun update(playbackState: PlaybackState)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM playback_state_table")
|
@Query("SELECT * FROM playback_state_table")
|
||||||
fun getAll(): List<PlaybackState>
|
fun getAll(): List<PlaybackState>
|
||||||
|
|
||||||
|
|
|
@ -285,6 +285,12 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
playbackManager.getStateFromDatabase(this@PlaybackService)
|
playbackManager.getStateFromDatabase(this@PlaybackService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d(this::class.simpleName, "notification restore")
|
||||||
|
|
||||||
|
// FIXME: Current position just will not show on notification when state is restored
|
||||||
|
restorePlayer()
|
||||||
|
restoreNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- OTHER FUNCTIONS ---
|
// --- OTHER FUNCTIONS ---
|
||||||
|
@ -302,6 +308,22 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun restoreNotification() {
|
||||||
|
playbackManager.song?.let {
|
||||||
|
uploadMetadataToSession(it)
|
||||||
|
notification.updateLoop(this)
|
||||||
|
notification.updateMode(this)
|
||||||
|
notification.updatePlaying(this)
|
||||||
|
notification.setMetadata(it, this) {
|
||||||
|
startForegroundOrNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stopForegroundAndNotification()
|
||||||
|
}
|
||||||
|
|
||||||
private fun uploadMetadataToSession(song: Song) {
|
private fun uploadMetadataToSession(song: Song) {
|
||||||
val builder = MediaMetadataCompat.Builder()
|
val builder = MediaMetadataCompat.Builder()
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)
|
||||||
|
|
|
@ -79,12 +79,15 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
init {
|
init {
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
|
|
||||||
// If the PlaybackViewModel was cleared [signified by the PlaybackStateManager having a
|
// If the PlaybackViewModel was cleared [Signified by PlaybackStateManager still being
|
||||||
// song and the fact that were are in the init function], then try to restore the playback
|
// around & the fact that we are in the init function], then attempt to restore the
|
||||||
// state.
|
// viewmodel state.
|
||||||
if (playbackManager.song != null) {
|
if (playbackManager.isRestored) {
|
||||||
restorePlaybackState()
|
restorePlaybackState()
|
||||||
} else {
|
} else {
|
||||||
|
// If [PlaybackStateManger] was also cleared [Due to Auxio's process dying], then
|
||||||
|
// attempt to restore it [Albeit PlaybackService will need to do the job as it
|
||||||
|
// has a context while PlaybackViewModel doesn't].
|
||||||
playbackManager.needContextToRestoreState()
|
playbackManager.needContextToRestoreState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -323,7 +326,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
mSong.value = playbackManager.song
|
mSong.value = playbackManager.song
|
||||||
mPosition.value = playbackManager.position / 1000
|
mPosition.value = playbackManager.position / 1000
|
||||||
|
mParent.value = playbackManager.parent
|
||||||
mQueue.value = playbackManager.queue
|
mQueue.value = playbackManager.queue
|
||||||
|
mMode.value = playbackManager.mode
|
||||||
mUserQueue.value = playbackManager.userQueue
|
mUserQueue.value = playbackManager.userQueue
|
||||||
mIndex.value = playbackManager.index
|
mIndex.value = playbackManager.index
|
||||||
mIsPlaying.value = playbackManager.isPlaying
|
mIsPlaying.value = playbackManager.isPlaying
|
||||||
|
|
|
@ -3,14 +3,6 @@ package org.oxycblt.auxio.playback.state
|
||||||
enum class LoopMode {
|
enum class LoopMode {
|
||||||
NONE, ONCE, INFINITE;
|
NONE, ONCE, INFINITE;
|
||||||
|
|
||||||
fun toConstant(): Int {
|
|
||||||
return when (this) {
|
|
||||||
NONE -> CONSTANT_NONE
|
|
||||||
ONCE -> CONSTANT_ONCE
|
|
||||||
INFINITE -> CONSTANT_INFINITE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun increment(): LoopMode {
|
fun increment(): LoopMode {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
NONE -> ONCE
|
NONE -> ONCE
|
||||||
|
@ -19,6 +11,14 @@ enum class LoopMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toConstant(): Int {
|
||||||
|
return when (this) {
|
||||||
|
NONE -> CONSTANT_NONE
|
||||||
|
ONCE -> CONSTANT_ONCE
|
||||||
|
INFINITE -> CONSTANT_INFINITE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CONSTANT_NONE = 0xA050
|
const val CONSTANT_NONE = 0xA050
|
||||||
const val CONSTANT_ONCE = 0xA051
|
const val CONSTANT_ONCE = 0xA051
|
||||||
|
|
|
@ -19,8 +19,8 @@ enum class PlaybackMode {
|
||||||
companion object {
|
companion object {
|
||||||
const val CONSTANT_IN_ARTIST = 0xA040
|
const val CONSTANT_IN_ARTIST = 0xA040
|
||||||
const val CONSTANT_IN_GENRE = 0xA041
|
const val CONSTANT_IN_GENRE = 0xA041
|
||||||
const val CONSTANT_IN_ALBUM = 0x4042
|
const val CONSTANT_IN_ALBUM = 0xA042
|
||||||
const val CONSTANT_ALL_SONGS = 0x4043
|
const val CONSTANT_ALL_SONGS = 0xA043
|
||||||
|
|
||||||
fun fromConstant(constant: Int): PlaybackMode? {
|
fun fromConstant(constant: Int): PlaybackMode? {
|
||||||
return when (constant) {
|
return when (constant) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import kotlin.random.Random
|
||||||
*
|
*
|
||||||
* All instantiation should be done with [PlaybackStateManager.from()].
|
* All instantiation should be done with [PlaybackStateManager.from()].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
|
* FIXME: Add started variable instead of relying on mSong being null
|
||||||
*/
|
*/
|
||||||
class PlaybackStateManager private constructor() {
|
class PlaybackStateManager private constructor() {
|
||||||
// Playback
|
// Playback
|
||||||
|
@ -84,6 +85,7 @@ class PlaybackStateManager private constructor() {
|
||||||
callbacks.forEach { it.onLoopUpdate(value) }
|
callbacks.forEach { it.onLoopUpdate(value) }
|
||||||
}
|
}
|
||||||
private var mIsInUserQueue = false
|
private var mIsInUserQueue = false
|
||||||
|
private var mIsRestored = false
|
||||||
|
|
||||||
val song: Song? get() = mSong
|
val song: Song? get() = mSong
|
||||||
val parent: BaseModel? get() = mParent
|
val parent: BaseModel? get() = mParent
|
||||||
|
@ -95,6 +97,7 @@ class PlaybackStateManager private constructor() {
|
||||||
val isPlaying: Boolean get() = mIsPlaying
|
val isPlaying: Boolean get() = mIsPlaying
|
||||||
val isShuffling: Boolean get() = mIsShuffling
|
val isShuffling: Boolean get() = mIsShuffling
|
||||||
val loopMode: LoopMode get() = mLoopMode
|
val loopMode: LoopMode get() = mLoopMode
|
||||||
|
val isRestored: Boolean get() = mIsRestored
|
||||||
|
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
|
@ -450,7 +453,7 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
// --- PERSISTENCE FUNCTIONS ---
|
// --- PERSISTENCE FUNCTIONS ---
|
||||||
// TODO: Persist queue edits?
|
// TODO: Persist queue edits?
|
||||||
// FIXME: Calling genShuffle without knowing the original queue edit from keepSong will cause issues.
|
// FIXME: Shuffling w/o knowing the original queue edit from keepSong will cause issues
|
||||||
|
|
||||||
fun needContextToRestoreState() {
|
fun needContextToRestoreState() {
|
||||||
callbacks.forEach { it.onNeedContextToRestoreState() }
|
callbacks.forEach { it.onNeedContextToRestoreState() }
|
||||||
|
@ -471,54 +474,25 @@ class PlaybackStateManager private constructor() {
|
||||||
suspend fun getStateFromDatabase(context: Context) {
|
suspend fun getStateFromDatabase(context: Context) {
|
||||||
Log.d(this::class.simpleName, "Getting state from DB.")
|
Log.d(this::class.simpleName, "Getting state from DB.")
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
val states = withContext(Dispatchers.IO) {
|
||||||
val database = PlaybackStateDatabase.getInstance(context)
|
val database = PlaybackStateDatabase.getInstance(context)
|
||||||
val states = database.playbackStateDAO.getAll()
|
val states = database.playbackStateDAO.getAll()
|
||||||
|
|
||||||
if (states.isEmpty()) {
|
|
||||||
Log.d(this::class.simpleName, "Nothing here. Not restoring.")
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
|
|
||||||
val state = states[0]
|
|
||||||
|
|
||||||
Log.d(this::class.simpleName, "Old state found, $state")
|
|
||||||
|
|
||||||
database.playbackStateDAO.clear()
|
database.playbackStateDAO.clear()
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
return@withContext states
|
||||||
val musicStore = MusicStore.getInstance()
|
|
||||||
|
|
||||||
mSong = musicStore.songs.find { it.id == state.songId }
|
|
||||||
mPosition = state.position
|
|
||||||
mParent = musicStore.parents.find { it.id == state.parentId }
|
|
||||||
mUserQueue = QueueConverter.fromString(state.userQueueIds)
|
|
||||||
mMode = PlaybackMode.fromConstant(state.mode) ?: PlaybackMode.ALL_SONGS
|
|
||||||
mLoopMode = LoopMode.fromConstant(state.loopMode) ?: LoopMode.NONE
|
|
||||||
mIsShuffling = state.isShuffling
|
|
||||||
mShuffleSeed = state.shuffleSeed
|
|
||||||
mIsInUserQueue = state.inUserQueue
|
|
||||||
|
|
||||||
mQueue = when (mMode) {
|
|
||||||
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist)
|
|
||||||
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album)
|
|
||||||
PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre)
|
|
||||||
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mIsShuffling) {
|
|
||||||
Log.d(this::class.simpleName, "You stupid fucking retard. JUST FUNCTION.")
|
|
||||||
mQueue.shuffle(Random(mShuffleSeed))
|
|
||||||
}
|
|
||||||
|
|
||||||
mIndex = state.index
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update PlaybackService outside of the main thread since its special for some reason
|
mIsRestored = true
|
||||||
callbacks.forEach {
|
|
||||||
it.onSeekConfirm(mPosition)
|
if (states.isEmpty()) {
|
||||||
|
Log.d(this::class.simpleName, "Nothing here. Not restoring.")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d(this::class.simpleName, "Old state found, ${states[0]}")
|
||||||
|
|
||||||
|
unpackFromPlaybackState(states[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun packToPlaybackState(): PlaybackState {
|
private fun packToPlaybackState(): PlaybackState {
|
||||||
|
@ -548,6 +522,64 @@ class PlaybackStateManager private constructor() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun unpackFromPlaybackState(playbackState: PlaybackState) {
|
||||||
|
val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
|
// Turn the simplified information from PlaybackState into values that can be used
|
||||||
|
mSong = musicStore.songs.find { it.id == playbackState.songId }
|
||||||
|
mPosition = playbackState.position
|
||||||
|
mParent = musicStore.parents.find { it.id == playbackState.parentId }
|
||||||
|
mUserQueue = QueueConverter.fromString(playbackState.userQueueIds)
|
||||||
|
mMode = PlaybackMode.fromConstant(playbackState.mode) ?: PlaybackMode.ALL_SONGS
|
||||||
|
mLoopMode = LoopMode.fromConstant(playbackState.loopMode) ?: LoopMode.NONE
|
||||||
|
mIsShuffling = playbackState.isShuffling
|
||||||
|
mShuffleSeed = playbackState.shuffleSeed
|
||||||
|
mIsInUserQueue = playbackState.inUserQueue
|
||||||
|
|
||||||
|
// If the parent was somehow dropped during saving, attempt to restore it.
|
||||||
|
mSong?.let {
|
||||||
|
if (mParent == null && mMode != PlaybackMode.ALL_SONGS) {
|
||||||
|
Log.d(this::class.simpleName, "Parent was corrupted while in mode $mMode. Attempting to restore.")
|
||||||
|
mParent = when (mMode) {
|
||||||
|
PlaybackMode.IN_ARTIST -> it.album.artist
|
||||||
|
PlaybackMode.IN_ALBUM -> it.album
|
||||||
|
else -> {
|
||||||
|
// If that fails, then just put the mode into all songs.
|
||||||
|
mMode = PlaybackMode.ALL_SONGS
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mIsShuffling) {
|
||||||
|
mQueue = when (mMode) {
|
||||||
|
PlaybackMode.IN_ARTIST -> (mParent as Artist).songs
|
||||||
|
PlaybackMode.IN_ALBUM -> (mParent as Album).songs
|
||||||
|
PlaybackMode.IN_GENRE -> (mParent as Genre).songs
|
||||||
|
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
mQueue.shuffle(Random(mShuffleSeed))
|
||||||
|
|
||||||
|
forceQueueUpdate()
|
||||||
|
} else {
|
||||||
|
mQueue = when (mMode) {
|
||||||
|
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist)
|
||||||
|
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album)
|
||||||
|
PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre)
|
||||||
|
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mIndex = playbackState.index
|
||||||
|
|
||||||
|
callbacks.forEach {
|
||||||
|
it.onSeekConfirm(mPosition)
|
||||||
|
it.onModeUpdate(mMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The interface for receiving updates from [PlaybackStateManager].
|
* The interface for receiving updates from [PlaybackStateManager].
|
||||||
* Add the callback to [PlaybackStateManager] using [addCallback],
|
* Add the callback to [PlaybackStateManager] using [addCallback],
|
||||||
|
|
Loading…
Reference in a new issue