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") @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,

View file

@ -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>

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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) { mIsRestored = true
Log.d(this::class.simpleName, "You stupid fucking retard. JUST FUNCTION.")
mQueue.shuffle(Random(mShuffleSeed)) if (states.isEmpty()) {
Log.d(this::class.simpleName, "Nothing here. Not restoring.")
return
} }
mIndex = state.index Log.d(this::class.simpleName, "Old state found, ${states[0]}")
}
}
// Update PlaybackService outside of the main thread since its special for some reason unpackFromPlaybackState(states[0])
callbacks.forEach {
it.onSeekConfirm(mPosition)
}
} }
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],