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:
parent
b8cc153f07
commit
ba48cdad29
1 changed files with 140 additions and 103 deletions
|
@ -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 ---
|
||||||
|
|
Loading…
Reference in a new issue