playback: make state manager lock

Add synchronized calls to all mutations in PlaybackStateManager.

I mean, it is global mutable state modified on several threads. This is
the safest option to remove a bunch of horrible bugs.
This commit is contained in:
OxygenCobalt 2022-06-19 16:42:54 -06:00
parent b8cc153f07
commit ba48cdad29
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47

View file

@ -47,8 +47,6 @@ import org.oxycblt.auxio.util.logW
* All access should be done with [PlaybackStateManager.getInstance]. * All access should be done with [PlaybackStateManager.getInstance].
* @author OxygenCobalt * @author OxygenCobalt
* *
* TODO: Leverage synchronized here to prevent state issues.
*
* TODO: Bug test app behavior when playback stops * TODO: Bug test app behavior when playback stops
*/ */
class PlaybackStateManager private constructor() { class PlaybackStateManager private constructor() {
@ -98,15 +96,17 @@ class PlaybackStateManager private constructor() {
/** Add a callback to this instance. Make sure to remove it when done. */ /** Add a callback to this instance. Make sure to remove it when done. */
fun addCallback(callback: Callback) { fun addCallback(callback: Callback) {
if (isInitialized) { synchronized(this) {
callback.onNewPlayback(index, queue, parent) if (isInitialized) {
callback.onPositionChanged(positionMs) callback.onNewPlayback(index, queue, parent)
callback.onPlayingChanged(isPlaying) callback.onPositionChanged(positionMs)
callback.onRepeatChanged(repeatMode) callback.onPlayingChanged(isPlaying)
callback.onShuffledChanged(isShuffled) callback.onRepeatChanged(repeatMode)
} callback.onShuffledChanged(isShuffled)
}
callbacks.add(callback) callbacks.add(callback)
}
} }
/** Remove a [PlaybackStateManager.Callback] bound to this instance. */ /** Remove a [PlaybackStateManager.Callback] bound to this instance. */
@ -121,15 +121,17 @@ class PlaybackStateManager private constructor() {
return return
} }
if (isInitialized) { synchronized(this) {
controller.loadSong(song) if (isInitialized) {
controller.seekTo(positionMs) controller.loadSong(song)
controller.onPlayingChanged(isPlaying) controller.seekTo(positionMs)
controller.onRepeatChanged(repeatMode) controller.onPlayingChanged(isPlaying)
controller.onPlayingChanged(isPlaying) controller.onRepeatChanged(repeatMode)
} controller.onPlayingChanged(isPlaying)
}
this.controller = controller this.controller = controller
}
} }
/** Unregister a [PlaybackStateManager.Controller] with this instance. */ /** Unregister a [PlaybackStateManager.Controller] with this instance. */
@ -151,20 +153,22 @@ class PlaybackStateManager private constructor() {
fun play(song: Song, playbackMode: PlaybackMode) { fun play(song: Song, playbackMode: PlaybackMode) {
val library = musicStore.library ?: return val library = musicStore.library ?: return
parent = synchronized(this) {
when (playbackMode) { parent =
PlaybackMode.ALL_SONGS -> null when (playbackMode) {
PlaybackMode.IN_ALBUM -> song.album PlaybackMode.ALL_SONGS -> null
PlaybackMode.IN_ARTIST -> song.album.artist PlaybackMode.IN_ALBUM -> song.album
PlaybackMode.IN_GENRE -> song.genre PlaybackMode.IN_ARTIST -> song.album.artist
} PlaybackMode.IN_GENRE -> song.genre
}
applyNewQueue(library, settingsManager.keepShuffle && isShuffled, song) applyNewQueue(library, settingsManager.keepShuffle && isShuffled, song)
seekTo(0) seekTo(0)
notifyNewPlayback() notifyNewPlayback()
notifyShuffledChanged() notifyShuffledChanged()
isPlaying = true isPlaying = true
isInitialized = true isInitialized = true
}
} }
/** /**
@ -173,48 +177,57 @@ class PlaybackStateManager private constructor() {
*/ */
fun play(parent: MusicParent, shuffled: Boolean) { fun play(parent: MusicParent, shuffled: Boolean) {
val library = musicStore.library ?: return val library = musicStore.library ?: return
this.parent = parent
applyNewQueue(library, shuffled, null) synchronized(this) {
seekTo(0) this.parent = parent
notifyNewPlayback() applyNewQueue(library, shuffled, null)
notifyShuffledChanged() seekTo(0)
isPlaying = true notifyNewPlayback()
isInitialized = true notifyShuffledChanged()
isPlaying = true
isInitialized = true
}
} }
/** Shuffle all songs. */ /** Shuffle all songs. */
fun shuffleAll() { fun shuffleAll() {
val library = musicStore.library ?: return val library = musicStore.library ?: return
parent = null synchronized(this) {
applyNewQueue(library, true, null) parent = null
seekTo(0) applyNewQueue(library, true, null)
notifyNewPlayback() seekTo(0)
notifyShuffledChanged() notifyNewPlayback()
isPlaying = true notifyShuffledChanged()
isInitialized = true isPlaying = true
isInitialized = true
}
} }
// --- QUEUE FUNCTIONS --- // --- QUEUE FUNCTIONS ---
/** Go to the next song, along with doing all the checks that entails. */ /** Go to the next song, along with doing all the checks that entails. */
fun next() { fun next() {
// Increment the index, if it cannot be incremented any further, then synchronized(this) {
// repeat and pause/resume playback depending on the setting // Increment the index, if it cannot be incremented any further, then
if (index < _queue.lastIndex) { // repeat and pause/resume playback depending on the setting
goto(index + 1, true) if (index < _queue.lastIndex) {
} else { goto(index + 1, true)
goto(0, repeatMode == RepeatMode.ALL) } else {
goto(0, repeatMode == RepeatMode.ALL)
}
} }
} }
/** Go to the previous song, doing any checks that are needed. */ /** Go to the previous song, doing any checks that are needed. */
fun prev() { fun prev() {
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms] synchronized(this) {
if (controller?.shouldPrevRewind() == true) { // If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
rewind() if (controller?.shouldPrevRewind() == true) {
isPlaying = true rewind()
} else { isPlaying = true
goto(max(index - 1, 0), true) } else {
goto(max(index - 1, 0), true)
}
} }
} }
@ -227,49 +240,65 @@ class PlaybackStateManager private constructor() {
/** Add a [song] to the top of the queue. */ /** Add a [song] to the top of the queue. */
fun playNext(song: Song) { fun playNext(song: Song) {
_queue.add(index + 1, song) synchronized(this) {
notifyQueueChanged() _queue.add(index + 1, song)
notifyQueueChanged()
}
} }
/** Add a list of [songs] to the top of the queue. */ /** Add a list of [songs] to the top of the queue. */
fun playNext(songs: List<Song>) { fun playNext(songs: List<Song>) {
_queue.addAll(index + 1, songs) synchronized(this) {
notifyQueueChanged() _queue.addAll(index + 1, songs)
notifyQueueChanged()
}
} }
/** Add a [song] to the end of the queue. */ /** Add a [song] to the end of the queue. */
fun addToQueue(song: Song) { fun addToQueue(song: Song) {
_queue.add(song) synchronized(this) {
notifyQueueChanged() _queue.add(song)
notifyQueueChanged()
}
} }
/** Add a list of [songs] to the end of the queue. */ /** Add a list of [songs] to the end of the queue. */
fun addToQueue(songs: List<Song>) { fun addToQueue(songs: List<Song>) {
_queue.addAll(songs) synchronized(this) {
notifyQueueChanged() _queue.addAll(songs)
notifyQueueChanged()
}
} }
/** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */ /** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */
fun moveQueueItem(from: Int, to: Int) { fun moveQueueItem(from: Int, to: Int) {
logD("Moving item $from to position $to") logD("Moving item $from to position $to")
_queue.add(to, _queue.removeAt(from))
notifyQueueChanged() synchronized(this) {
_queue.add(to, _queue.removeAt(from))
notifyQueueChanged()
}
} }
/** Remove a queue item at [index]. Will ignore invalid indexes. */ /** Remove a queue item at [index]. Will ignore invalid indexes. */
fun removeQueueItem(index: Int) { fun removeQueueItem(index: Int) {
logD("Removing item ${_queue[index].rawName}") logD("Removing item ${_queue[index].rawName}")
_queue.removeAt(index)
notifyQueueChanged() synchronized(this) {
_queue.removeAt(index)
notifyQueueChanged()
}
} }
/** Set whether this instance is [shuffled]. Updates the queue accordingly. */ /** Set whether this instance is [shuffled]. Updates the queue accordingly. */
fun reshuffle(shuffled: Boolean) { fun reshuffle(shuffled: Boolean) {
val library = musicStore.library ?: return val library = musicStore.library ?: return
val song = song ?: return synchronized(this) {
applyNewQueue(library, shuffled, song) val song = song ?: return
notifyQueueChanged() applyNewQueue(library, shuffled, song)
notifyShuffledChanged() notifyQueueChanged()
notifyShuffledChanged()
}
} }
private fun applyNewQueue(library: MusicStore.Library, shuffled: Boolean, keep: Song?) { private fun applyNewQueue(library: MusicStore.Library, shuffled: Boolean, keep: Song?) {
@ -318,11 +347,13 @@ class PlaybackStateManager private constructor() {
return return
} }
// Don't accept any bugged positions that are over the duration of the song. synchronized(this) {
val maxDuration = song?.durationMs ?: -1 // Don't accept any bugged positions that are over the duration of the song.
if (positionMs <= maxDuration) { val maxDuration = song?.durationMs ?: -1
this.positionMs = positionMs if (positionMs <= maxDuration) {
notifyPositionChanged() this.positionMs = positionMs
notifyPositionChanged()
}
} }
} }
@ -331,9 +362,11 @@ class PlaybackStateManager private constructor() {
* @param positionMs The position to seek to in millis. * @param positionMs The position to seek to in millis.
*/ */
fun seekTo(positionMs: Long) { fun seekTo(positionMs: Long) {
this.positionMs = positionMs synchronized(this) {
controller?.seekTo(positionMs) this.positionMs = positionMs
notifyPositionChanged() controller?.seekTo(positionMs)
notifyPositionChanged()
}
} }
/** Rewind to the beginning of a song. */ /** Rewind to the beginning of a song. */
@ -343,7 +376,9 @@ class PlaybackStateManager private constructor() {
fun repeat() { fun repeat() {
rewind() rewind()
if (settingsManager.pauseOnRepeat) { if (settingsManager.pauseOnRepeat) {
isPlaying = false synchronized(this) {
isPlaying = false
}
} }
} }
@ -368,20 +403,22 @@ class PlaybackStateManager private constructor() {
logD("State read completed successfully in ${System.currentTimeMillis() - start}ms") logD("State read completed successfully in ${System.currentTimeMillis() - start}ms")
if (state != null) { synchronized(this) {
index = state.index if (state != null) {
parent = state.parent index = state.index
_queue = state.queue.toMutableList() parent = state.parent
repeatMode = state.repeatMode _queue = state.queue.toMutableList()
isShuffled = state.isShuffled repeatMode = state.repeatMode
isShuffled = state.isShuffled
notifyNewPlayback() notifyNewPlayback()
seekTo(state.positionMs) seekTo(state.positionMs)
notifyRepeatModeChanged() notifyRepeatModeChanged()
notifyShuffledChanged() notifyShuffledChanged()
}
isInitialized = true
} }
isInitialized = true
} }
/** /**
@ -397,19 +434,19 @@ class PlaybackStateManager private constructor() {
val database = PlaybackStateDatabase.getInstance(context) val database = PlaybackStateDatabase.getInstance(context)
database.write( database.write(
PlaybackStateDatabase.SavedState( synchronized(this) {
index = index, PlaybackStateDatabase.SavedState(
parent = parent, index = index,
queue = _queue, parent = parent,
positionMs = positionMs, queue = _queue,
isShuffled = isShuffled, positionMs = positionMs,
repeatMode = repeatMode)) isShuffled = isShuffled,
repeatMode = repeatMode)
})
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
} }
// --- CALLBACKS --- // --- CALLBACKS ---