Fix persistence bugs

Fix some edge cases with the persistent PlaybackState restoration.
This commit is contained in:
OxygenCobalt 2020-11-15 20:07:40 -07:00
parent 6d809f4303
commit 212ffbf2c7
7 changed files with 117 additions and 62 deletions

View file

@ -7,10 +7,10 @@ import androidx.room.PrimaryKey
@Entity(tableName = "playback_state_table")
data class PlaybackState(
@PrimaryKey(autoGenerate = true)
var id: Long = 0L,
var id: Long = Long.MIN_VALUE,
@ColumnInfo(name = "song_id")
val songId: Long = -1L,
val songId: Long = Long.MIN_VALUE,
@ColumnInfo(name = "position")
val position: Long,

View file

@ -3,16 +3,12 @@ package org.oxycblt.auxio.database
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
@Dao
interface PlaybackStateDAO {
@Insert
fun insert(playbackState: PlaybackState)
@Update
fun update(playbackState: PlaybackState)
@Query("SELECT * FROM playback_state_table")
fun getAll(): List<PlaybackState>

View file

@ -285,6 +285,12 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
serviceScope.launch {
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 ---
@ -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) {
val builder = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)

View file

@ -79,12 +79,15 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
init {
playbackManager.addCallback(this)
// If the PlaybackViewModel was cleared [signified by the PlaybackStateManager having a
// song and the fact that were are in the init function], then try to restore the playback
// state.
if (playbackManager.song != null) {
// If the PlaybackViewModel was cleared [Signified by PlaybackStateManager still being
// around & the fact that we are in the init function], then attempt to restore the
// viewmodel state.
if (playbackManager.isRestored) {
restorePlaybackState()
} 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()
}
}
@ -323,7 +326,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
mSong.value = playbackManager.song
mPosition.value = playbackManager.position / 1000
mParent.value = playbackManager.parent
mQueue.value = playbackManager.queue
mMode.value = playbackManager.mode
mUserQueue.value = playbackManager.userQueue
mIndex.value = playbackManager.index
mIsPlaying.value = playbackManager.isPlaying

View file

@ -3,14 +3,6 @@ package org.oxycblt.auxio.playback.state
enum class LoopMode {
NONE, ONCE, INFINITE;
fun toConstant(): Int {
return when (this) {
NONE -> CONSTANT_NONE
ONCE -> CONSTANT_ONCE
INFINITE -> CONSTANT_INFINITE
}
}
fun increment(): LoopMode {
return when (this) {
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 {
const val CONSTANT_NONE = 0xA050
const val CONSTANT_ONCE = 0xA051

View file

@ -19,8 +19,8 @@ enum class PlaybackMode {
companion object {
const val CONSTANT_IN_ARTIST = 0xA040
const val CONSTANT_IN_GENRE = 0xA041
const val CONSTANT_IN_ALBUM = 0x4042
const val CONSTANT_ALL_SONGS = 0x4043
const val CONSTANT_IN_ALBUM = 0xA042
const val CONSTANT_ALL_SONGS = 0xA043
fun fromConstant(constant: Int): PlaybackMode? {
return when (constant) {

View file

@ -24,6 +24,7 @@ import kotlin.random.Random
*
* All instantiation should be done with [PlaybackStateManager.from()].
* @author OxygenCobalt
* FIXME: Add started variable instead of relying on mSong being null
*/
class PlaybackStateManager private constructor() {
// Playback
@ -84,6 +85,7 @@ class PlaybackStateManager private constructor() {
callbacks.forEach { it.onLoopUpdate(value) }
}
private var mIsInUserQueue = false
private var mIsRestored = false
val song: Song? get() = mSong
val parent: BaseModel? get() = mParent
@ -95,6 +97,7 @@ class PlaybackStateManager private constructor() {
val isPlaying: Boolean get() = mIsPlaying
val isShuffling: Boolean get() = mIsShuffling
val loopMode: LoopMode get() = mLoopMode
val isRestored: Boolean get() = mIsRestored
// --- CALLBACKS ---
@ -450,7 +453,7 @@ class PlaybackStateManager private constructor() {
// --- PERSISTENCE FUNCTIONS ---
// 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() {
callbacks.forEach { it.onNeedContextToRestoreState() }
@ -471,54 +474,25 @@ class PlaybackStateManager private constructor() {
suspend fun getStateFromDatabase(context: Context) {
Log.d(this::class.simpleName, "Getting state from DB.")
withContext(Dispatchers.IO) {
val states = withContext(Dispatchers.IO) {
val database = PlaybackStateDatabase.getInstance(context)
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()
withContext(Dispatchers.Main) {
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
}
return@withContext states
}
// Update PlaybackService outside of the main thread since its special for some reason
callbacks.forEach {
it.onSeekConfirm(mPosition)
mIsRestored = true
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 {
@ -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].
* Add the callback to [PlaybackStateManager] using [addCallback],