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].
* @author OxygenCobalt
*
* TODO: Leverage synchronized here to prevent state issues.
*
* TODO: Bug test app behavior when playback stops
*/
class PlaybackStateManager private constructor() {
@ -98,6 +96,7 @@ class PlaybackStateManager private constructor() {
/** Add a callback to this instance. Make sure to remove it when done. */
fun addCallback(callback: Callback) {
synchronized(this) {
if (isInitialized) {
callback.onNewPlayback(index, queue, parent)
callback.onPositionChanged(positionMs)
@ -108,6 +107,7 @@ class PlaybackStateManager private constructor() {
callbacks.add(callback)
}
}
/** Remove a [PlaybackStateManager.Callback] bound to this instance. */
fun removeCallback(callback: Callback) {
@ -121,6 +121,7 @@ class PlaybackStateManager private constructor() {
return
}
synchronized(this) {
if (isInitialized) {
controller.loadSong(song)
controller.seekTo(positionMs)
@ -131,6 +132,7 @@ class PlaybackStateManager private constructor() {
this.controller = controller
}
}
/** Unregister a [PlaybackStateManager.Controller] with this instance. */
fun unregisterController(controller: Controller) {
@ -151,6 +153,7 @@ class PlaybackStateManager private constructor() {
fun play(song: Song, playbackMode: PlaybackMode) {
val library = musicStore.library ?: return
synchronized(this) {
parent =
when (playbackMode) {
PlaybackMode.ALL_SONGS -> null
@ -166,6 +169,7 @@ class PlaybackStateManager private constructor() {
isPlaying = true
isInitialized = true
}
}
/**
* Play a [parent], such as an artist or album.
@ -173,6 +177,8 @@ class PlaybackStateManager private constructor() {
*/
fun play(parent: MusicParent, shuffled: Boolean) {
val library = musicStore.library ?: return
synchronized(this) {
this.parent = parent
applyNewQueue(library, shuffled, null)
seekTo(0)
@ -181,10 +187,12 @@ class PlaybackStateManager private constructor() {
isPlaying = true
isInitialized = true
}
}
/** Shuffle all songs. */
fun shuffleAll() {
val library = musicStore.library ?: return
synchronized(this) {
parent = null
applyNewQueue(library, true, null)
seekTo(0)
@ -193,11 +201,13 @@ class PlaybackStateManager private constructor() {
isPlaying = true
isInitialized = true
}
}
// --- QUEUE FUNCTIONS ---
/** Go to the next song, along with doing all the checks that entails. */
fun next() {
synchronized(this) {
// Increment the index, if it cannot be incremented any further, then
// repeat and pause/resume playback depending on the setting
if (index < _queue.lastIndex) {
@ -206,9 +216,11 @@ class PlaybackStateManager private constructor() {
goto(0, repeatMode == RepeatMode.ALL)
}
}
}
/** Go to the previous song, doing any checks that are needed. */
fun prev() {
synchronized(this) {
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
if (controller?.shouldPrevRewind() == true) {
rewind()
@ -217,6 +229,7 @@ class PlaybackStateManager private constructor() {
goto(max(index - 1, 0), true)
}
}
}
private fun goto(idx: Int, play: Boolean) {
index = idx
@ -227,50 +240,66 @@ class PlaybackStateManager private constructor() {
/** Add a [song] to the top of the queue. */
fun playNext(song: Song) {
synchronized(this) {
_queue.add(index + 1, song)
notifyQueueChanged()
}
}
/** Add a list of [songs] to the top of the queue. */
fun playNext(songs: List<Song>) {
synchronized(this) {
_queue.addAll(index + 1, songs)
notifyQueueChanged()
}
}
/** Add a [song] to the end of the queue. */
fun addToQueue(song: Song) {
synchronized(this) {
_queue.add(song)
notifyQueueChanged()
}
}
/** Add a list of [songs] to the end of the queue. */
fun addToQueue(songs: List<Song>) {
synchronized(this) {
_queue.addAll(songs)
notifyQueueChanged()
}
}
/** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */
fun moveQueueItem(from: Int, to: Int) {
logD("Moving item $from to position $to")
synchronized(this) {
_queue.add(to, _queue.removeAt(from))
notifyQueueChanged()
}
}
/** Remove a queue item at [index]. Will ignore invalid indexes. */
fun removeQueueItem(index: Int) {
logD("Removing item ${_queue[index].rawName}")
synchronized(this) {
_queue.removeAt(index)
notifyQueueChanged()
}
}
/** Set whether this instance is [shuffled]. Updates the queue accordingly. */
fun reshuffle(shuffled: Boolean) {
val library = musicStore.library ?: return
synchronized(this) {
val song = song ?: return
applyNewQueue(library, shuffled, song)
notifyQueueChanged()
notifyShuffledChanged()
}
}
private fun applyNewQueue(library: MusicStore.Library, shuffled: Boolean, keep: Song?) {
val newQueue = (parent?.songs ?: library.songs).toMutableList()
@ -318,6 +347,7 @@ class PlaybackStateManager private constructor() {
return
}
synchronized(this) {
// Don't accept any bugged positions that are over the duration of the song.
val maxDuration = song?.durationMs ?: -1
if (positionMs <= maxDuration) {
@ -325,16 +355,19 @@ class PlaybackStateManager private constructor() {
notifyPositionChanged()
}
}
}
/**
* **Seek** to a [positionMs].
* @param positionMs The position to seek to in millis.
*/
fun seekTo(positionMs: Long) {
synchronized(this) {
this.positionMs = positionMs
controller?.seekTo(positionMs)
notifyPositionChanged()
}
}
/** Rewind to the beginning of a song. */
fun rewind() = seekTo(0)
@ -343,9 +376,11 @@ class PlaybackStateManager private constructor() {
fun repeat() {
rewind()
if (settingsManager.pauseOnRepeat) {
synchronized(this) {
isPlaying = false
}
}
}
// --- PERSISTENCE FUNCTIONS ---
@ -368,6 +403,7 @@ class PlaybackStateManager private constructor() {
logD("State read completed successfully in ${System.currentTimeMillis() - start}ms")
synchronized(this) {
if (state != null) {
index = state.index
parent = state.parent
@ -383,6 +419,7 @@ class PlaybackStateManager private constructor() {
isInitialized = true
}
}
/**
* Save the current state to the database.
@ -397,19 +434,19 @@ class PlaybackStateManager private constructor() {
val database = PlaybackStateDatabase.getInstance(context)
database.write(
synchronized(this) {
PlaybackStateDatabase.SavedState(
index = index,
parent = parent,
queue = _queue,
positionMs = positionMs,
isShuffled = isShuffled,
repeatMode = repeatMode))
repeatMode = repeatMode)
})
this@PlaybackStateManager.logD(
"State save completed successfully in ${System.currentTimeMillis() - start}ms")
}
isInitialized = true
}
// --- CALLBACKS ---