playback: save playback state on every change
Prior, I was saving when the service was closed, which is a stupid decision and caused a lot of unreliability. Resolves #404.
This commit is contained in:
parent
b2d9b244e5
commit
1766283cd2
3 changed files with 55 additions and 74 deletions
|
@ -44,8 +44,10 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.list.ListSettings
|
import org.oxycblt.auxio.list.ListSettings
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
@ -117,6 +119,7 @@ class PlaybackService :
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val restoreScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
private val restoreScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
||||||
private val saveScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
private val saveScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
||||||
|
private var currentSaveJob: Job? = null
|
||||||
|
|
||||||
// --- SERVICE OVERRIDES ---
|
// --- SERVICE OVERRIDES ---
|
||||||
|
|
||||||
|
@ -228,8 +231,7 @@ class PlaybackService :
|
||||||
player.isPlaying,
|
player.isPlaying,
|
||||||
// The position value can be below zero or past the expected duration, make
|
// The position value can be below zero or past the expected duration, make
|
||||||
// sure we handle that.
|
// sure we handle that.
|
||||||
player.currentPosition.coerceAtLeast(0)
|
player.currentPosition.coerceAtLeast(0).coerceAtMost(it.song.durationMs))
|
||||||
.coerceAtMost(it.song.durationMs))
|
|
||||||
}
|
}
|
||||||
?: Progression.nil()
|
?: Progression.nil()
|
||||||
|
|
||||||
|
@ -246,11 +248,12 @@ class PlaybackService :
|
||||||
|
|
||||||
override fun resolveQueue(): RawQueue {
|
override fun resolveQueue(): RawQueue {
|
||||||
val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it).song }
|
val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it).song }
|
||||||
val shuffledMapping = if (player.shuffleModeEnabled) {
|
val shuffledMapping =
|
||||||
player.unscrambleQueueIndices()
|
if (player.shuffleModeEnabled) {
|
||||||
} else {
|
player.unscrambleQueueIndices()
|
||||||
emptyList()
|
} else {
|
||||||
}
|
emptyList()
|
||||||
|
}
|
||||||
return RawQueue(heap, shuffledMapping, player.currentMediaItemIndex)
|
return RawQueue(heap, shuffledMapping, player.currentMediaItemIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,11 +282,13 @@ class PlaybackService :
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.play()
|
player.play()
|
||||||
playbackManager.ack(this, StateAck.NewPlayback)
|
playbackManager.ack(this, StateAck.NewPlayback)
|
||||||
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playing(playing: Boolean) {
|
override fun playing(playing: Boolean) {
|
||||||
player.playWhenReady = playing
|
player.playWhenReady = playing
|
||||||
// Dispatched later once all of the changes have been accumulated
|
// Dispatched later once all of the changes have been accumulated
|
||||||
|
// Playing state is not persisted, do not need to save
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun repeatMode(repeatMode: RepeatMode) {
|
override fun repeatMode(repeatMode: RepeatMode) {
|
||||||
|
@ -295,15 +300,19 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
playbackManager.ack(this, StateAck.RepeatModeChanged)
|
playbackManager.ack(this, StateAck.RepeatModeChanged)
|
||||||
updatePauseOnRepeat()
|
updatePauseOnRepeat()
|
||||||
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekTo(positionMs: Long) {
|
override fun seekTo(positionMs: Long) {
|
||||||
player.seekTo(positionMs)
|
player.seekTo(positionMs)
|
||||||
|
// Dispatched later once all of the changes have been accumulated
|
||||||
|
// Deferred save is handled on position discontinuity
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun next() {
|
override fun next() {
|
||||||
player.seekToNext()
|
player.seekToNext()
|
||||||
playbackManager.ack(this, StateAck.IndexMoved)
|
playbackManager.ack(this, StateAck.IndexMoved)
|
||||||
|
// Deferred save is handled on position discontinuity
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prev() {
|
override fun prev() {
|
||||||
|
@ -313,6 +322,7 @@ class PlaybackService :
|
||||||
player.seekToPreviousMediaItem()
|
player.seekToPreviousMediaItem()
|
||||||
}
|
}
|
||||||
playbackManager.ack(this, StateAck.IndexMoved)
|
playbackManager.ack(this, StateAck.IndexMoved)
|
||||||
|
// Deferred save is handled on position discontinuity
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun goto(index: Int) {
|
override fun goto(index: Int) {
|
||||||
|
@ -324,6 +334,7 @@ class PlaybackService :
|
||||||
val trueIndex = indices[index]
|
val trueIndex = indices[index]
|
||||||
player.seekTo(trueIndex, C.TIME_UNSET)
|
player.seekTo(trueIndex, C.TIME_UNSET)
|
||||||
playbackManager.ack(this, StateAck.IndexMoved)
|
playbackManager.ack(this, StateAck.IndexMoved)
|
||||||
|
// Deferred save is handled on position discontinuity
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shuffled(shuffled: Boolean) {
|
override fun shuffled(shuffled: Boolean) {
|
||||||
|
@ -335,16 +346,19 @@ class PlaybackService :
|
||||||
BetterShuffleOrder(player.mediaItemCount, player.currentMediaItemIndex))
|
BetterShuffleOrder(player.mediaItemCount, player.currentMediaItemIndex))
|
||||||
}
|
}
|
||||||
playbackManager.ack(this, StateAck.QueueReordered)
|
playbackManager.ack(this, StateAck.QueueReordered)
|
||||||
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
|
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
|
||||||
player.addMediaItems(player.nextMediaItemIndex, songs.map { it.toMediaItem() })
|
player.addMediaItems(player.nextMediaItemIndex, songs.map { it.toMediaItem() })
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
|
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
|
||||||
player.addMediaItems(songs.map { it.toMediaItem() })
|
player.addMediaItems(songs.map { it.toMediaItem() })
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun move(from: Int, to: Int, ack: StateAck.Move) {
|
override fun move(from: Int, to: Int, ack: StateAck.Move) {
|
||||||
|
@ -367,6 +381,7 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(at: Int, ack: StateAck.Remove) {
|
override fun remove(at: Int, ack: StateAck.Remove) {
|
||||||
|
@ -378,6 +393,7 @@ class PlaybackService :
|
||||||
val trueIndex = indices[at]
|
val trueIndex = indices[at]
|
||||||
player.removeMediaItem(trueIndex)
|
player.removeMediaItem(trueIndex)
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
||||||
|
@ -480,6 +496,17 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPositionDiscontinuity(
|
||||||
|
oldPosition: Player.PositionInfo,
|
||||||
|
newPosition: Player.PositionInfo,
|
||||||
|
reason: Int
|
||||||
|
) {
|
||||||
|
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||||
|
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||||
|
deferSave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
super.onEvents(player, events)
|
super.onEvents(player, events)
|
||||||
|
|
||||||
|
@ -580,6 +607,21 @@ class PlaybackService :
|
||||||
|
|
||||||
// --- OTHER FUNCTIONS ---
|
// --- OTHER FUNCTIONS ---
|
||||||
|
|
||||||
|
private fun deferSave() {
|
||||||
|
currentSaveJob?.let {
|
||||||
|
logD("Discarding prior save job")
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
|
currentSaveJob =
|
||||||
|
saveScope.launch {
|
||||||
|
logD("Waiting for save buffer")
|
||||||
|
delay(SAVE_BUFFER)
|
||||||
|
yield()
|
||||||
|
logD("Committing saved state")
|
||||||
|
persistenceRepository.saveState(playbackManager.toSavedState())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun broadcastAudioEffectAction(event: String) {
|
private fun broadcastAudioEffectAction(event: String) {
|
||||||
logD("Broadcasting AudioEffect event: $event")
|
logD("Broadcasting AudioEffect event: $event")
|
||||||
sendBroadcast(
|
sendBroadcast(
|
||||||
|
@ -589,18 +631,6 @@ class PlaybackService :
|
||||||
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
|
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopAndSave() {
|
|
||||||
// This session has ended, so we need to reset this flag for when the next session starts.
|
|
||||||
hasPlayed = false
|
|
||||||
if (foregroundManager.tryStopForeground()) {
|
|
||||||
// Now that we have ended the foreground state (and thus music playback), we'll need
|
|
||||||
// to save the current state as it's not long until this service (and likely the whole
|
|
||||||
// app) is killed.
|
|
||||||
logD("Saving playback state")
|
|
||||||
saveScope.launch { persistenceRepository.saveState(playbackManager.toSavedState()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require
|
* A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require
|
||||||
* an active [IntentFilter] to be registered.
|
* an active [IntentFilter] to be registered.
|
||||||
|
@ -657,7 +687,10 @@ class PlaybackService :
|
||||||
ACTION_EXIT -> {
|
ACTION_EXIT -> {
|
||||||
logD("Received exit event")
|
logD("Received exit event")
|
||||||
playbackManager.playing(false)
|
playbackManager.playing(false)
|
||||||
stopAndSave()
|
// This session has ended, so we need to reset this flag for when the next
|
||||||
|
// session starts.
|
||||||
|
hasPlayed = false
|
||||||
|
foregroundManager.tryStopForeground()
|
||||||
}
|
}
|
||||||
WidgetProvider.ACTION_WIDGET_UPDATE -> {
|
WidgetProvider.ACTION_WIDGET_UPDATE -> {
|
||||||
logD("Received widget update event")
|
logD("Received widget update event")
|
||||||
|
@ -687,6 +720,7 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val SAVE_BUFFER = 5000L
|
||||||
const val ACTION_INC_REPEAT_MODE = BuildConfig.APPLICATION_ID + ".action.LOOP"
|
const val ACTION_INC_REPEAT_MODE = BuildConfig.APPLICATION_ID + ".action.LOOP"
|
||||||
const val ACTION_INVERT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE"
|
const val ACTION_INVERT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE"
|
||||||
const val ACTION_SKIP_PREV = BuildConfig.APPLICATION_ID + ".action.PREV"
|
const val ACTION_SKIP_PREV = BuildConfig.APPLICATION_ID + ".action.PREV"
|
||||||
|
|
|
@ -84,41 +84,6 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
|
||||||
}
|
}
|
||||||
getString(R.string.set_key_reindex) -> musicModel.refresh()
|
getString(R.string.set_key_reindex) -> musicModel.refresh()
|
||||||
getString(R.string.set_key_rescan) -> musicModel.rescan()
|
getString(R.string.set_key_rescan) -> musicModel.rescan()
|
||||||
getString(R.string.set_key_save_state) -> {
|
|
||||||
playbackModel.savePlaybackState { saved ->
|
|
||||||
// Use the nullable context, as we could try to show a toast when this
|
|
||||||
// fragment is no longer attached.
|
|
||||||
logD("Showing saving confirmation")
|
|
||||||
if (saved) {
|
|
||||||
context?.showToast(R.string.lbl_state_saved)
|
|
||||||
} else {
|
|
||||||
context?.showToast(R.string.err_did_not_save)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getString(R.string.set_key_wipe_state) -> {
|
|
||||||
playbackModel.wipePlaybackState { wiped ->
|
|
||||||
logD("Showing wipe confirmation")
|
|
||||||
if (wiped) {
|
|
||||||
// Use the nullable context, as we could try to show a toast when this
|
|
||||||
// fragment is no longer attached.
|
|
||||||
context?.showToast(R.string.lbl_state_wiped)
|
|
||||||
} else {
|
|
||||||
context?.showToast(R.string.err_did_not_wipe)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getString(R.string.set_key_restore_state) ->
|
|
||||||
playbackModel.tryRestorePlaybackState { restored ->
|
|
||||||
logD("Showing restore confirmation")
|
|
||||||
if (restored) {
|
|
||||||
// Use the nullable context, as we could try to show a toast when this
|
|
||||||
// fragment is no longer attached.
|
|
||||||
context?.showToast(R.string.lbl_state_restored)
|
|
||||||
} else {
|
|
||||||
context?.showToast(R.string.err_did_not_restore)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> return super.onPreferenceTreeClick(preference)
|
else -> return super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,23 +44,5 @@
|
||||||
app:title="@string/set_rescan" />
|
app:title="@string/set_rescan" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/set_state">
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
app:key="@string/set_key_save_state"
|
|
||||||
app:summary="@string/set_save_desc"
|
|
||||||
app:title="@string/set_save_state" />
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
app:key="@string/set_key_wipe_state"
|
|
||||||
app:summary="@string/set_wipe_desc"
|
|
||||||
app:title="@string/set_wipe_state" />
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
app:key="@string/set_key_restore_state"
|
|
||||||
app:summary="@string/set_restore_desc"
|
|
||||||
app:title="@string/set_restore_state" />
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
Reference in a new issue