playback: hide playbackstatemanaager impl

Make PlaybackStateManager an interface instead of a direct
implementation.

Part of a rework to implement dependency injection in-app.
This commit is contained in:
Alexander Capehart 2023-01-29 15:46:41 -07:00
parent c655f7d39e
commit bb2ea9df27
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
14 changed files with 543 additions and 551 deletions

View file

@ -68,25 +68,11 @@ class Library(rawSongs: List<Song.Raw>, settings: MusicSettings) {
fun sanitize(song: Song) = find<Song>(song.uid)
/**
* Convert a [Album] from an another library into a [Album] in this [Library].
* @param album The [Album] to convert.
* Convert a [MusicParent] from an another library into a [MusicParent] in this [Library].
* @param parent The [MusicParent] to convert.
* @return The analogous [Album] in this [Library], or null if it does not exist.
*/
fun sanitize(album: Album) = find<Album>(album.uid)
/**
* Convert a [Artist] from an another library into a [Artist] in this [Library].
* @param artist The [Artist] to convert.
* @return The analogous [Artist] in this [Library], or null if it does not exist.
*/
fun sanitize(artist: Artist) = find<Artist>(artist.uid)
/**
* Convert a [Genre] from an another library into a [Genre] in this [Library].
* @param genre The [Genre] to convert.
* @return The analogous [Genre] in this [Library], or null if it does not exist.
*/
fun sanitize(genre: Genre) = find<Genre>(genre.uid)
fun <T : MusicParent> sanitize(parent: T) = find<T>(parent.uid)
/**
* Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri].

View file

@ -24,7 +24,6 @@ import android.os.Build
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.oxycblt.auxio.BuildConfig

View file

@ -56,7 +56,7 @@ import org.oxycblt.auxio.util.logD
class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
private val indexer = Indexer.getInstance()
private val musicStore = MusicStore.getInstance()
private val playbackManager = PlaybackStateManager.getInstance()
private val playbackManager = PlaybackStateManager.get()
private val serviceJob = Job()
private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
private var currentIndexJob: Job? = null
@ -139,7 +139,18 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
// Clear invalid models from PlaybackStateManager. This is not connected
// to a listener as it is bad practice for a shared object to attach to
// the listener system of another.
playbackManager.sanitize(newLibrary)
playbackManager.toSavedState()?.let { savedState ->
playbackManager.applySavedState(
PlaybackStateManager.SavedState(
parent = savedState.parent?.let(newLibrary::sanitize),
queueState =
savedState.queueState.remap { song ->
newLibrary.sanitize(requireNotNull(song))
},
positionMs = savedState.positionMs,
repeatMode = savedState.repeatMode),
true)
}
}
// Forward the new library to MusicStore to continue the update process.
musicStore.library = newLibrary

View file

@ -38,7 +38,7 @@ class PlaybackViewModel(application: Application) :
AndroidViewModel(application), PlaybackStateManager.Listener {
private val musicSettings = MusicSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
private val playbackManager = PlaybackStateManager.getInstance()
private val playbackManager = PlaybackStateManager.get()
private val persistenceRepository = PersistenceRepository.from(application)
private val musicStore = MusicStore.getInstance()
private var lastPositionJob: Job? = null
@ -430,8 +430,7 @@ class PlaybackViewModel(application: Application) :
*/
fun savePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch {
val saved = playbackManager.saveState(persistenceRepository)
onDone(saved)
onDone(persistenceRepository.saveState(playbackManager.toSavedState()))
}
}
@ -440,10 +439,7 @@ class PlaybackViewModel(application: Application) :
* @param onDone Called when the wipe is completed with true if successful, and false otherwise.
*/
fun wipePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch {
val wiped = playbackManager.wipeState(persistenceRepository)
onDone(wiped)
}
viewModelScope.launch { onDone(persistenceRepository.saveState(null)) }
}
/**
@ -453,8 +449,16 @@ class PlaybackViewModel(application: Application) :
*/
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch {
val restored = playbackManager.restoreState(persistenceRepository, true)
onDone(restored)
val library = musicStore.library
if (library != null) {
val savedState = persistenceRepository.readState(library)
if (savedState != null) {
playbackManager.applySavedState(savedState, true)
onDone(true)
return@launch
}
}
onDone(false)
}
}

View file

@ -73,6 +73,7 @@ abstract class PersistenceDatabase : RoomDatabase() {
PersistenceDatabase::class.java,
"auxio_playback_persistence.db")
.fallbackToDestructiveMigration()
.fallbackToDestructiveMigrationFrom(1)
.fallbackToDestructiveMigrationOnDowngrade()
.build()
INSTANCE = newInstance

View file

@ -21,8 +21,9 @@ import android.content.Context
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
/**
* Manages the persisted playback state in a structured manner.
@ -30,30 +31,16 @@ import org.oxycblt.auxio.util.logD
*/
interface PersistenceRepository {
/**
* Read the previously persisted [SavedState].
* @param library The [Library] required to de-serialize the [SavedState].
* Read the previously persisted [PlaybackStateManager.SavedState].
* @param library The [Library] required to de-serialize the [PlaybackStateManager.SavedState].
*/
suspend fun readState(library: Library): SavedState?
suspend fun readState(library: Library): PlaybackStateManager.SavedState?
/**
* Persist a new [SavedState].
* @param state The [SavedState] to persist.
* Persist a new [PlaybackStateManager.SavedState].
* @param state The [PlaybackStateManager.SavedState] to persist.
*/
suspend fun saveState(state: SavedState?)
/**
* A condensed representation of the playback state that can be persisted.
* @param parent The [MusicParent] item currently being played from.
* @param queueState The [Queue.SavedState]
* @param positionMs The current position in the currently played song, in ms
* @param repeatMode The current [RepeatMode].
*/
data class SavedState(
val parent: MusicParent?,
val queueState: Queue.SavedState,
val positionMs: Long,
val repeatMode: RepeatMode,
)
suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean
companion object {
/**
@ -69,10 +56,19 @@ private class RealPersistenceRepository(private val context: Context) : Persiste
private val playbackStateDao: PlaybackStateDao by lazy { database.playbackStateDao() }
private val queueDao: QueueDao by lazy { database.queueDao() }
override suspend fun readState(library: Library): PersistenceRepository.SavedState? {
val playbackState = playbackStateDao.getState() ?: return null
val heap = queueDao.getHeap()
val mapping = queueDao.getMapping()
override suspend fun readState(library: Library): PlaybackStateManager.SavedState? {
val playbackState: PlaybackState
val heap: List<QueueHeapItem>
val mapping: List<QueueMappingItem>
try {
playbackState = playbackStateDao.getState() ?: return null
heap = queueDao.getHeap()
mapping = queueDao.getMapping()
} catch (e: Exception) {
logE("Unable to load playback state data")
logE(e.stackTraceToString())
return null
}
val orderedMapping = mutableListOf<Int>()
val shuffledMapping = mutableListOf<Int>()
@ -82,8 +78,9 @@ private class RealPersistenceRepository(private val context: Context) : Persiste
}
val parent = playbackState.parentUid?.let { library.find<MusicParent>(it) }
logD("Read playback state")
return PersistenceRepository.SavedState(
return PlaybackStateManager.SavedState(
parent = parent,
queueState =
Queue.SavedState(
@ -96,12 +93,18 @@ private class RealPersistenceRepository(private val context: Context) : Persiste
repeatMode = playbackState.repeatMode)
}
override suspend fun saveState(state: PersistenceRepository.SavedState?) {
override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean {
// Only bother saving a state if a song is actively playing from one.
// This is not the case with a null state.
playbackStateDao.nukeState()
queueDao.nukeHeap()
queueDao.nukeMapping()
try {
playbackStateDao.nukeState()
queueDao.nukeHeap()
queueDao.nukeMapping()
} catch (e: Exception) {
logE("Unable to clear previous state")
logE(e.stackTraceToString())
return false
}
logD("Cleared state")
if (state != null) {
// Transform saved state into raw state, which can then be written to the database.
@ -113,22 +116,29 @@ private class RealPersistenceRepository(private val context: Context) : Persiste
repeatMode = state.repeatMode,
songUid = state.queueState.songUid,
parentUid = state.parent?.uid)
playbackStateDao.insertState(playbackState)
// Convert the remaining queue information do their database-specific counterparts.
val heap =
state.queueState.heap.mapIndexed { i, song ->
QueueHeapItem(i, requireNotNull(song).uid)
}
queueDao.insertHeap(heap)
val mapping =
state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed {
i,
pair ->
QueueMappingItem(i, pair.first, pair.second)
}
queueDao.insertMapping(mapping)
try {
playbackStateDao.insertState(playbackState)
queueDao.insertHeap(heap)
queueDao.insertMapping(mapping)
} catch (e: Exception) {
logE("Unable to write new state")
logE(e.stackTraceToString())
return false
}
logD("Wrote state")
}
return true
}
}

View file

@ -36,30 +36,86 @@ import org.oxycblt.auxio.music.Song
*
* @author OxygenCobalt
*/
class Queue {
interface Queue {
val index: Int
val currentSong: Song?
val isShuffled: Boolean
/**
* Resolve this queue into a more conventional list of [Song]s.
* @return A list of [Song] corresponding to the current queue mapping.
*/
fun resolve(): List<Song>
/**
* Represents the possible changes that can occur during certain queue mutation events. The
* precise meanings of these differ somewhat depending on the type of mutation done.
*/
enum class ChangeResult {
/** Only the mapping has changed. */
MAPPING,
/** The mapping has changed, and the index also changed to align with it. */
INDEX,
/**
* The current song has changed, possibly alongside the mapping and index depending on the
* context.
*/
SONG
}
/**
* An immutable representation of the queue state.
* @param heap The heap of [Song]s that are/were used in the queue. This can be modified with
* null values to represent [Song]s that were "lost" from the heap without having to change
* other values.
* @param orderedMapping The mapping of the [heap] to an ordered queue.
* @param shuffledMapping The mapping of the [heap] to a shuffled queue.
* @param index The index of the currently playing [Song] at the time of serialization.
* @param songUid The [Music.UID] of the [Song] that was originally at [index].
*/
class SavedState(
val heap: List<Song?>,
val orderedMapping: List<Int>,
val shuffledMapping: List<Int>,
val index: Int,
val songUid: Music.UID,
) {
/**
* Remaps the [heap] of this instance based on the given mapping function and copies it into
* a new [SavedState].
* @param transform Code to remap the existing [Song] heap into a new [Song] heap. This
* **MUST** be the same size as the original heap. [Song] instances that could not be
* converted should be replaced with null in the new heap.
* @throws IllegalStateException If the invariant specified by [transform] is violated.
*/
inline fun remap(transform: (Song?) -> Song?) =
SavedState(heap.map(transform), orderedMapping, shuffledMapping, index, songUid)
}
}
class EditableQueue : Queue {
@Volatile private var heap = mutableListOf<Song>()
@Volatile private var orderedMapping = mutableListOf<Int>()
@Volatile private var shuffledMapping = mutableListOf<Int>()
/** The index of the currently playing [Song] in the current mapping. */
@Volatile
var index = -1
override var index = -1
private set
/** The currently playing [Song]. */
val currentSong: Song?
override val currentSong: Song?
get() =
shuffledMapping
.ifEmpty { orderedMapping.ifEmpty { null } }
?.getOrNull(index)
?.let(heap::get)
/** Whether this queue is shuffled. */
val isShuffled: Boolean
override val isShuffled: Boolean
get() = shuffledMapping.isNotEmpty()
/**
* Resolve this queue into a more conventional list of [Song]s.
* @return A list of [Song] corresponding to the current queue mapping.
*/
fun resolve() =
override fun resolve() =
if (currentSong != null) {
shuffledMapping.map { heap[it] }.ifEmpty { orderedMapping.map { heap[it] } }
} else {
@ -137,11 +193,11 @@ class Queue {
* @return [ChangeResult.MAPPING] if added to an existing queue, or [ChangeResult.SONG] if there
* was no prior playback and these enqueued [Song]s start new playback.
*/
fun playNext(songs: List<Song>): ChangeResult {
fun playNext(songs: List<Song>): Queue.ChangeResult {
if (orderedMapping.isEmpty()) {
// No playback, start playing these songs.
start(songs[0], songs, false)
return ChangeResult.SONG
return Queue.ChangeResult.SONG
}
val heapIndices = songs.map(::addSongToHeap)
@ -156,20 +212,21 @@ class Queue {
orderedMapping.addAll(index + 1, heapIndices)
}
check()
return ChangeResult.MAPPING
return Queue.ChangeResult.MAPPING
}
/**
* Add [Song]s to the end of the queue. Will start playback if nothing is playing.
* @param songs The [Song]s to add.
* @return [ChangeResult.MAPPING] if added to an existing queue, or [ChangeResult.SONG] if there
* was no prior playback and these enqueued [Song]s start new playback.
* @return [Queue.ChangeResult.MAPPING] if added to an existing queue, or
* [Queue.ChangeResult.SONG] if there was no prior playback and these enqueued [Song]s start new
* playback.
*/
fun addToQueue(songs: List<Song>): ChangeResult {
fun addToQueue(songs: List<Song>): Queue.ChangeResult {
if (orderedMapping.isEmpty()) {
// No playback, start playing these songs.
start(songs[0], songs, false)
return ChangeResult.SONG
return Queue.ChangeResult.SONG
}
val heapIndices = songs.map(::addSongToHeap)
@ -179,18 +236,18 @@ class Queue {
shuffledMapping.addAll(heapIndices)
}
check()
return ChangeResult.MAPPING
return Queue.ChangeResult.MAPPING
}
/**
* Move a [Song] at the given position to a new position.
* @param src The position of the [Song] to move.
* @param dst The destination position of the [Song].
* @return [ChangeResult.MAPPING] if the move occurred after the current index,
* [ChangeResult.INDEX] if the move occurred before or at the current index, requiring it to be
* mutated.
* @return [Queue.ChangeResult.MAPPING] if the move occurred after the current index,
* [Queue.ChangeResult.INDEX] if the move occurred before or at the current index, requiring it
* to be mutated.
*/
fun move(src: Int, dst: Int): ChangeResult {
fun move(src: Int, dst: Int): Queue.ChangeResult {
if (shuffledMapping.isNotEmpty()) {
// Move songs only in the shuffled mapping. There is no sane analogous form of
// this for the ordered mapping.
@ -210,21 +267,21 @@ class Queue {
else -> {
// Nothing to do.
check()
return ChangeResult.MAPPING
return Queue.ChangeResult.MAPPING
}
}
check()
return ChangeResult.INDEX
return Queue.ChangeResult.INDEX
}
/**
* Remove a [Song] at the given position.
* @param at The position of the [Song] to remove.
* @return [ChangeResult.MAPPING] if the removed [Song] was after the current index,
* [ChangeResult.INDEX] if the removed [Song] was before the current index, and
* [ChangeResult.SONG] if the currently playing [Song] was removed.
* @return [Queue.ChangeResult.MAPPING] if the removed [Song] was after the current index,
* [Queue.ChangeResult.INDEX] if the removed [Song] was before the current index, and
* [Queue.ChangeResult.SONG] if the currently playing [Song] was removed.
*/
fun remove(at: Int): ChangeResult {
fun remove(at: Int): Queue.ChangeResult {
if (shuffledMapping.isNotEmpty()) {
// Remove the specified index in the shuffled mapping and the analogous song in the
// ordered mapping.
@ -242,34 +299,34 @@ class Queue {
val result =
when {
// We just removed the currently playing song.
index == at -> ChangeResult.SONG
index == at -> Queue.ChangeResult.SONG
// Index was ahead of removed song, shift back to preserve consistency.
index > at -> {
index -= 1
ChangeResult.INDEX
Queue.ChangeResult.INDEX
}
// Nothing to do
else -> ChangeResult.MAPPING
else -> Queue.ChangeResult.MAPPING
}
check()
return result
}
/**
* Convert the current state of this instance into a [SavedState].
* @return A new [SavedState] reflecting the exact state of the queue when called.
* Convert the current state of this instance into a [Queue.SavedState].
* @return A new [Queue.SavedState] reflecting the exact state of the queue when called.
*/
fun toSavedState() =
currentSong?.let { song ->
SavedState(
Queue.SavedState(
heap.toList(), orderedMapping.toList(), shuffledMapping.toList(), index, song.uid)
}
/**
* Update this instance from the given [SavedState].
* @param savedState A [SavedState] with a valid queue representation.
* Update this instance from the given [Queue.SavedState].
* @param savedState A [Queue.SavedState] with a valid queue representation.
*/
fun applySavedState(savedState: SavedState) {
fun applySavedState(savedState: Queue.SavedState) {
val adjustments = mutableListOf<Int?>()
var currentShift = 0
for (song in savedState.heap) {
@ -345,49 +402,4 @@ class Queue {
"Queue inconsistency detected: Shuffled mapping indices out of heap bounds"
}
}
/**
* An immutable representation of the queue state.
* @param heap The heap of [Song]s that are/were used in the queue. This can be modified with
* null values to represent [Song]s that were "lost" from the heap without having to change
* other values.
* @param orderedMapping The mapping of the [heap] to an ordered queue.
* @param shuffledMapping The mapping of the [heap] to a shuffled queue.
* @param index The index of the currently playing [Song] at the time of serialization.
* @param songUid The [Music.UID] of the [Song] that was originally at [index].
*/
class SavedState(
val heap: List<Song?>,
val orderedMapping: List<Int>,
val shuffledMapping: List<Int>,
val index: Int,
val songUid: Music.UID,
) {
/**
* Remaps the [heap] of this instance based on the given mapping function and copies it into
* a new [SavedState].
* @param transform Code to remap the existing [Song] heap into a new [Song] heap. This
* **MUST** be the same size as the original heap. [Song] instances that could not be
* converted should be replaced with null in the new heap.
* @throws IllegalStateException If the invariant specified by [transform] is violated.
*/
inline fun remap(transform: (Song?) -> Song?) =
SavedState(heap.map(transform), orderedMapping, shuffledMapping, index, songUid)
}
/**
* Represents the possible changes that can occur during certain queue mutation events. The
* precise meanings of these differ somewhat depending on the type of mutation done.
*/
enum class ChangeResult {
/** Only the mapping has changed. */
MAPPING,
/** The mapping has changed, and the index also changed to align with it. */
INDEX,
/**
* The current song has changed, possibly alongside the mapping and index depending on the
* context.
*/
SONG
}
}

View file

@ -31,7 +31,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
* @author Alexander Capehart (OxygenCobalt)
*/
class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
private val playbackManager = PlaybackStateManager.getInstance()
private val playbackManager = PlaybackStateManager.get()
private val _queue = MutableStateFlow(listOf<Song>())
/** The current queue. */

View file

@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.logD
*/
class ReplayGainAudioProcessor(context: Context) :
BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener {
private val playbackManager = PlaybackStateManager.getInstance()
private val playbackManager = PlaybackStateManager.get()
private val playbackSettings = PlaybackSettings.from(context)
private var lastFormat: Format? = null

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 Auxio Project
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -17,19 +17,13 @@
package org.oxycblt.auxio.playback.state
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.playback.persist.PersistenceRepository
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.EditableQueue
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* Core playback state controller class.
@ -38,48 +32,29 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* MediaSession is poorly designed. This class instead ful-fills this role.
*
* This should ***NOT*** be used outside of the playback module.
* - If you want to use the playback state in the UI, use
* [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs.
* - If you want to use the playback state in the UI, use PlaybackViewModel as it can withstand
* volatile UIs.
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use
* [org.oxycblt.auxio.playback.system.PlaybackService].
* PlaybackService.
*
* Internal consumers should usually use [Listener], however the component that manages the player
* itself should instead use [InternalPlayer].
*
* All access should be done with [PlaybackStateManager.getInstance].
* All access should be done with [get].
*
* @author Alexander Capehart (OxygenCobalt)
*/
class PlaybackStateManager private constructor() {
private val musicStore = MusicStore.getInstance()
private val listeners = mutableListOf<Listener>()
@Volatile private var internalPlayer: InternalPlayer? = null
@Volatile private var pendingAction: InternalPlayer.Action? = null
@Volatile private var isInitialized = false
interface PlaybackStateManager {
/** The current [Queue]. */
val queue = Queue()
val queue: Queue
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
@Volatile
var parent: MusicParent? = null // FIXME: Parent is interpreted wrong when nothing is playing.
private set
val parent: MusicParent?
/** The current [InternalPlayer] state. */
@Volatile
var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
private set
val playerState: InternalPlayer.State
/** The current [RepeatMode] */
@Volatile
var repeatMode = RepeatMode.NONE
set(value) {
field = value
notifyRepeatModeChanged()
}
/**
* The current audio session ID of the internal player. Null if [InternalPlayer] is unavailable.
*/
var repeatMode: RepeatMode
/** The audio session ID of the internal player. Null if no internal player exists. */
val currentAudioSessionId: Int?
get() = internalPlayer?.audioSessionId
/**
* Add a [Listener] to this instance. This can be used to receive changes in the playback state.
@ -87,16 +62,7 @@ class PlaybackStateManager private constructor() {
* @param listener The [Listener] to add.
* @see Listener
*/
@Synchronized
fun addListener(listener: Listener) {
if (isInitialized) {
listener.onNewPlayback(queue, parent)
listener.onRepeatChanged(repeatMode)
listener.onStateChanged(playerState)
}
listeners.add(listener)
}
fun addListener(listener: Listener)
/**
* Remove a [Listener] from this instance, preventing it from receiving any further updates.
@ -104,10 +70,7 @@ class PlaybackStateManager private constructor() {
* the first place.
* @see Listener
*/
@Synchronized
fun removeListener(listener: Listener) {
listeners.remove(listener)
}
fun removeListener(listener: Listener)
/**
* Register an [InternalPlayer] for this instance. This instance will handle translating the
@ -116,42 +79,15 @@ class PlaybackStateManager private constructor() {
* @param internalPlayer The [InternalPlayer] to register. Will do nothing if already
* registered.
*/
@Synchronized
fun registerInternalPlayer(internalPlayer: InternalPlayer) {
if (this.internalPlayer != null) {
logW("Internal player is already registered")
return
}
if (isInitialized) {
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
internalPlayer.seekTo(playerState.calculateElapsedPositionMs())
// See if there's any action that has been queued.
requestAction(internalPlayer)
// Once initialized, try to synchronize with the player state it has created.
synchronizeState(internalPlayer)
}
this.internalPlayer = internalPlayer
}
fun registerInternalPlayer(internalPlayer: InternalPlayer)
/**
* Unregister the [InternalPlayer] from this instance, prevent it from recieving any further
* Unregister the [InternalPlayer] from this instance, prevent it from receiving any further
* commands.
* @param internalPlayer The [InternalPlayer] to unregister. Must be the current
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
*/
@Synchronized
fun unregisterInternalPlayer(internalPlayer: InternalPlayer) {
if (this.internalPlayer !== internalPlayer) {
logW("Given internal player did not match current internal player")
return
}
this.internalPlayer = null
}
// --- PLAYING FUNCTIONS ---
fun unregisterInternalPlayer(internalPlayer: InternalPlayer)
/**
* Start new playback.
@ -161,190 +97,81 @@ class PlaybackStateManager private constructor() {
* collection of "All [Song]s".
* @param shuffled Whether to shuffle or not.
*/
@Synchronized
fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean) {
val internalPlayer = internalPlayer ?: return
// Set up parent and queue
this.parent = parent
this.queue.start(song, queue, shuffled)
// Notify components of changes
notifyNewPlayback()
internalPlayer.loadSong(this.queue.currentSong, true)
// Played something, so we are initialized now
isInitialized = true
}
// --- QUEUE FUNCTIONS ---
fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean)
/**
* Go to the next [Song] in the queue. Will go to the first [Song] in the queue if there is no
* [Song] ahead to skip to.
*/
@Synchronized
fun next() {
val internalPlayer = internalPlayer ?: return
var play = true
if (!queue.goto(queue.index + 1)) {
queue.goto(0)
play = repeatMode == RepeatMode.ALL
}
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, play)
}
fun next()
/**
* Go to the previous [Song] in the queue. Will rewind if there are no previous [Song]s to skip
* to, or if configured to do so.
*/
@Synchronized
fun prev() {
val internalPlayer = internalPlayer ?: return
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
if (internalPlayer.shouldRewindWithPrev) {
rewind()
setPlaying(true)
} else {
if (!queue.goto(queue.index - 1)) {
queue.goto(0)
}
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true)
}
}
fun prev()
/**
* Play a [Song] at the given position in the queue.
* @param index The position of the [Song] in the queue to start playing.
*/
@Synchronized
fun goto(index: Int) {
val internalPlayer = internalPlayer ?: return
if (queue.goto(index)) {
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true)
}
}
/**
* Add a [Song] to the top of the queue.
* @param song The [Song] to add.
*/
@Synchronized fun playNext(song: Song) = playNext(listOf(song))
fun goto(index: Int)
/**
* Add [Song]s to the top of the queue.
* @param songs The [Song]s to add.
*/
@Synchronized
fun playNext(songs: List<Song>) {
val internalPlayer = internalPlayer ?: return
when (queue.playNext(songs)) {
Queue.ChangeResult.MAPPING -> notifyQueueChanged(Queue.ChangeResult.MAPPING)
Queue.ChangeResult.SONG -> {
// Enqueueing actually started a new playback session from all songs.
parent = null
internalPlayer.loadSong(queue.currentSong, true)
notifyNewPlayback()
}
Queue.ChangeResult.INDEX -> error("Unreachable")
}
}
fun playNext(songs: List<Song>)
/**
* Add a [Song] to the end of the queue.
* Add a [Song] to the top of the queue.
* @param song The [Song] to add.
*/
@Synchronized fun addToQueue(song: Song) = addToQueue(listOf(song))
fun playNext(song: Song) = playNext(listOf(song))
/**
* Add [Song]s to the end of the queue.
* @param songs The [Song]s to add.
*/
@Synchronized
fun addToQueue(songs: List<Song>) {
val internalPlayer = internalPlayer ?: return
when (queue.addToQueue(songs)) {
Queue.ChangeResult.MAPPING -> notifyQueueChanged(Queue.ChangeResult.MAPPING)
Queue.ChangeResult.SONG -> {
// Enqueueing actually started a new playback session from all songs.
parent = null
internalPlayer.loadSong(queue.currentSong, true)
notifyNewPlayback()
}
Queue.ChangeResult.INDEX -> error("Unreachable")
}
}
fun addToQueue(songs: List<Song>)
/**
* Add a [Song] to the end of the queue.
* @param song The [Song] to add.
*/
fun addToQueue(song: Song) = addToQueue(listOf(song))
/**
* Move a [Song] in the queue.
* @param src The position of the [Song] to move in the queue.
* @param dst The destination position in the queue.
*/
@Synchronized
fun moveQueueItem(src: Int, dst: Int) {
logD("Moving item $src to position $dst")
notifyQueueChanged(queue.move(src, dst))
}
fun moveQueueItem(src: Int, dst: Int)
/**
* Remove a [Song] from the queue.
* @param at The position of the [Song] to remove in the queue.
*/
@Synchronized
fun removeQueueItem(at: Int) {
val internalPlayer = internalPlayer ?: return
logD("Removing item at $at")
val change = queue.remove(at)
if (change == Queue.ChangeResult.SONG) {
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
}
notifyQueueChanged(change)
}
fun removeQueueItem(at: Int)
/**
* (Re)shuffle or (Re)order this instance.
* @param shuffled Whether to shuffle the queue or not.
*/
@Synchronized
fun reorder(shuffled: Boolean) {
queue.reorder(shuffled)
notifyQueueReordered()
}
// --- INTERNAL PLAYER FUNCTIONS ---
fun reorder(shuffled: Boolean)
/**
* Synchronize the state of this instance with the current [InternalPlayer].
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
*/
@Synchronized
fun synchronizeState(internalPlayer: InternalPlayer) {
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
logW("Given internal player did not match current internal player")
return
}
val newState = internalPlayer.getState(queue.currentSong?.durationMs ?: 0)
if (newState != playerState) {
playerState = newState
notifyStateChanged()
}
}
fun synchronizeState(internalPlayer: InternalPlayer)
/**
* Start a [InternalPlayer.Action] for the current [InternalPlayer] to handle eventually.
* @param action The [InternalPlayer.Action] to perform.
*/
@Synchronized
fun startAction(action: InternalPlayer.Action) {
val internalPlayer = internalPlayer
if (internalPlayer == null || !internalPlayer.performAction(action)) {
logD("Internal player not present or did not consume action, waiting")
pendingAction = action
}
}
fun startAction(action: InternalPlayer.Action)
/**
* Request that the pending [InternalPlayer.Action] (if any) be passed to the given
@ -352,213 +179,37 @@ class PlaybackStateManager private constructor() {
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
*/
@Synchronized
fun requestAction(internalPlayer: InternalPlayer) {
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
logW("Given internal player did not match current internal player")
return
}
if (pendingAction?.let(internalPlayer::performAction) == true) {
logD("Pending action consumed")
pendingAction = null
}
}
fun requestAction(internalPlayer: InternalPlayer)
/**
* Update whether playback is ongoing or not.
* @param isPlaying Whether playback is ongoing or not.
*/
fun setPlaying(isPlaying: Boolean) {
internalPlayer?.setPlaying(isPlaying)
}
fun setPlaying(isPlaying: Boolean)
/**
* Seek to the given position in the currently playing [Song].
* @param positionMs The position to seek to, in milliseconds.
*/
@Synchronized
fun seekTo(positionMs: Long) {
internalPlayer?.seekTo(positionMs)
}
fun seekTo(positionMs: Long)
/** Rewind to the beginning of the currently playing [Song]. */
fun rewind() = seekTo(0)
// --- PERSISTENCE FUNCTIONS ---
/**
* Converts the current state of this instance into a [SavedState].
* @return An immutable [SavedState] that is analogous to the current state, or null if nothing
* is currently playing.
*/
fun toSavedState(): SavedState?
/**
* Restore the previously saved state (if any) and apply it to the playback state.
* @param repository The [PersistenceRepository] to load from.
* @param force Whether to do a restore regardless of any prior playback state.
* @return If the state was restored, false otherwise.
* Restores this instance from the given [SavedState].
* @param savedState The [SavedState] to restore from.
* @param destructive Whether to disregard the prior playback state and overwrite it with this
* [SavedState].
*/
suspend fun restoreState(repository: PersistenceRepository, force: Boolean): Boolean {
if (isInitialized && !force) {
// Already initialized and not forcing a restore, nothing to do.
return false
}
val library = musicStore.library ?: return false
val internalPlayer = internalPlayer ?: return false
val state =
try {
withContext(Dispatchers.IO) { repository.readState(library) }
} catch (e: Exception) {
logE("Unable to restore playback state.")
logE(e.stackTraceToString())
return false
}
// Translate the state we have just read into a usable playback state for this
// instance.
return synchronized(this) {
// State could have changed while we were loading, so check if we were initialized
// now before applying the state.
if (state != null && (!isInitialized || force)) {
parent = state.parent
queue.applySavedState(state.queueState)
repeatMode = state.repeatMode
notifyNewPlayback()
notifyRepeatModeChanged()
// Continuing playback after drastic state updates is a bad idea, so pause.
internalPlayer.loadSong(queue.currentSong, false)
internalPlayer.seekTo(state.positionMs)
isInitialized = true
true
} else {
false
}
}
}
/**
* Save the current state.
* @param database The [PersistenceRepository] to save the state to.
* @return If state was saved, false otherwise.
*/
suspend fun saveState(database: PersistenceRepository): Boolean {
logD("Saving state to DB")
// Create the saved state from the current playback state.
val state =
synchronized(this) {
queue.toSavedState()?.let {
PersistenceRepository.SavedState(
parent = parent,
queueState = it,
positionMs = playerState.calculateElapsedPositionMs(),
repeatMode = repeatMode)
}
}
return try {
withContext(Dispatchers.IO) { database.saveState(state) }
true
} catch (e: Exception) {
logE("Unable to save playback state.")
logE(e.stackTraceToString())
false
}
}
/**
* Clear the current state.
* @param repository The [PersistenceRepository] to clear the state from
* @return If the state was cleared, false otherwise.
*/
suspend fun wipeState(repository: PersistenceRepository) =
try {
logD("Wiping state")
withContext(Dispatchers.IO) { repository.saveState(null) }
true
} catch (e: Exception) {
logE("Unable to wipe playback state.")
logE(e.stackTraceToString())
false
}
/**
* Update the playback state to align with a new [Library].
* @param newLibrary The new [Library] that was recently loaded.
*/
@Synchronized
fun sanitize(newLibrary: Library) {
if (!isInitialized) {
// Nothing playing, nothing to do.
logD("Not initialized, no need to sanitize")
return
}
val internalPlayer = internalPlayer ?: return
logD("Sanitizing state")
// While we could just save and reload the state, we instead sanitize the state
// at runtime for better performance (and to sidestep a co-routine on behalf of the caller).
// Sanitize parent
parent =
parent?.let {
when (it) {
is Album -> newLibrary.sanitize(it)
is Artist -> newLibrary.sanitize(it)
is Genre -> newLibrary.sanitize(it)
}
}
// Sanitize the queue.
queue.toSavedState()?.let { state ->
queue.applySavedState(state.remap { newLibrary.sanitize(unlikelyToBeNull(it)) })
}
notifyNewPlayback()
val oldPosition = playerState.calculateElapsedPositionMs()
// Continuing playback while also possibly doing drastic state updates is
// a bad idea, so pause.
internalPlayer.loadSong(queue.currentSong, false)
if (queue.currentSong != null) {
// Internal player may have reloaded the media item, re-seek to the previous position
seekTo(oldPosition)
}
}
// --- CALLBACKS ---
private fun notifyIndexMoved() {
for (callback in listeners) {
callback.onIndexMoved(queue)
}
}
private fun notifyQueueChanged(change: Queue.ChangeResult) {
for (callback in listeners) {
callback.onQueueChanged(queue, change)
}
}
private fun notifyQueueReordered() {
for (callback in listeners) {
callback.onQueueReordered(queue)
}
}
private fun notifyNewPlayback() {
for (callback in listeners) {
callback.onNewPlayback(queue, parent)
}
}
private fun notifyStateChanged() {
for (callback in listeners) {
callback.onStateChanged(playerState)
}
}
private fun notifyRepeatModeChanged() {
for (callback in listeners) {
callback.onRepeatChanged(repeatMode)
}
}
fun applySavedState(savedState: SavedState, destructive: Boolean)
/**
* The interface for receiving updates from [PlaybackStateManager]. Add the listener to
@ -606,6 +257,20 @@ class PlaybackStateManager private constructor() {
fun onRepeatChanged(repeatMode: RepeatMode) {}
}
/**
* A condensed representation of the playback state that can be persisted.
* @param parent The [MusicParent] item currently being played from.
* @param queueState The [Queue.SavedState]
* @param positionMs The current position in the currently played song, in ms
* @param repeatMode The current [RepeatMode].
*/
data class SavedState(
val parent: MusicParent?,
val queueState: Queue.SavedState,
val positionMs: Long,
val repeatMode: RepeatMode,
)
companion object {
@Volatile private var INSTANCE: PlaybackStateManager? = null
@ -613,7 +278,7 @@ class PlaybackStateManager private constructor() {
* Get a singleton instance.
* @return The (possibly newly-created) singleton instance.
*/
fun getInstance(): PlaybackStateManager {
fun get(): PlaybackStateManager {
val currentInstance = INSTANCE
if (currentInstance != null) {
@ -621,10 +286,310 @@ class PlaybackStateManager private constructor() {
}
synchronized(this) {
val newInstance = PlaybackStateManager()
val newInstance = RealPlaybackStateManager()
INSTANCE = newInstance
return newInstance
}
}
}
}
private class RealPlaybackStateManager : PlaybackStateManager {
private val listeners = mutableListOf<PlaybackStateManager.Listener>()
@Volatile private var internalPlayer: InternalPlayer? = null
@Volatile private var pendingAction: InternalPlayer.Action? = null
@Volatile private var isInitialized = false
override val queue = EditableQueue()
@Volatile
override var parent: MusicParent? =
null // FIXME: Parent is interpreted wrong when nothing is playing.
private set
@Volatile
override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
private set
@Volatile
override var repeatMode = RepeatMode.NONE
set(value) {
field = value
notifyRepeatModeChanged()
}
override val currentAudioSessionId: Int?
get() = internalPlayer?.audioSessionId
@Synchronized
override fun addListener(listener: PlaybackStateManager.Listener) {
if (isInitialized) {
listener.onNewPlayback(queue, parent)
listener.onRepeatChanged(repeatMode)
listener.onStateChanged(playerState)
}
listeners.add(listener)
}
@Synchronized
override fun removeListener(listener: PlaybackStateManager.Listener) {
listeners.remove(listener)
}
@Synchronized
override fun registerInternalPlayer(internalPlayer: InternalPlayer) {
if (this.internalPlayer != null) {
logW("Internal player is already registered")
return
}
if (isInitialized) {
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
internalPlayer.seekTo(playerState.calculateElapsedPositionMs())
// See if there's any action that has been queued.
requestAction(internalPlayer)
// Once initialized, try to synchronize with the player state it has created.
synchronizeState(internalPlayer)
}
this.internalPlayer = internalPlayer
}
@Synchronized
override fun unregisterInternalPlayer(internalPlayer: InternalPlayer) {
if (this.internalPlayer !== internalPlayer) {
logW("Given internal player did not match current internal player")
return
}
this.internalPlayer = null
}
// --- PLAYING FUNCTIONS ---
@Synchronized
override fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean) {
val internalPlayer = internalPlayer ?: return
// Set up parent and queue
this.parent = parent
this.queue.start(song, queue, shuffled)
// Notify components of changes
notifyNewPlayback()
internalPlayer.loadSong(this.queue.currentSong, true)
// Played something, so we are initialized now
isInitialized = true
}
// --- QUEUE FUNCTIONS ---
@Synchronized
override fun next() {
val internalPlayer = internalPlayer ?: return
var play = true
if (!queue.goto(queue.index + 1)) {
queue.goto(0)
play = repeatMode == RepeatMode.ALL
}
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, play)
}
@Synchronized
override fun prev() {
val internalPlayer = internalPlayer ?: return
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
if (internalPlayer.shouldRewindWithPrev) {
rewind()
setPlaying(true)
} else {
if (!queue.goto(queue.index - 1)) {
queue.goto(0)
}
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true)
}
}
@Synchronized
override fun goto(index: Int) {
val internalPlayer = internalPlayer ?: return
if (queue.goto(index)) {
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true)
}
}
@Synchronized
override fun playNext(songs: List<Song>) {
val internalPlayer = internalPlayer ?: return
when (queue.playNext(songs)) {
Queue.ChangeResult.MAPPING -> notifyQueueChanged(Queue.ChangeResult.MAPPING)
Queue.ChangeResult.SONG -> {
// Enqueueing actually started a new playback session from all songs.
parent = null
internalPlayer.loadSong(queue.currentSong, true)
notifyNewPlayback()
}
Queue.ChangeResult.INDEX -> error("Unreachable")
}
}
@Synchronized
override fun addToQueue(songs: List<Song>) {
val internalPlayer = internalPlayer ?: return
when (queue.addToQueue(songs)) {
Queue.ChangeResult.MAPPING -> notifyQueueChanged(Queue.ChangeResult.MAPPING)
Queue.ChangeResult.SONG -> {
// Enqueueing actually started a new playback session from all songs.
parent = null
internalPlayer.loadSong(queue.currentSong, true)
notifyNewPlayback()
}
Queue.ChangeResult.INDEX -> error("Unreachable")
}
}
@Synchronized
override fun moveQueueItem(src: Int, dst: Int) {
logD("Moving item $src to position $dst")
notifyQueueChanged(queue.move(src, dst))
}
@Synchronized
override fun removeQueueItem(at: Int) {
val internalPlayer = internalPlayer ?: return
logD("Removing item at $at")
val change = queue.remove(at)
if (change == Queue.ChangeResult.SONG) {
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
}
notifyQueueChanged(change)
}
@Synchronized
override fun reorder(shuffled: Boolean) {
queue.reorder(shuffled)
notifyQueueReordered()
}
// --- INTERNAL PLAYER FUNCTIONS ---
@Synchronized
override fun synchronizeState(internalPlayer: InternalPlayer) {
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
logW("Given internal player did not match current internal player")
return
}
val newState = internalPlayer.getState(queue.currentSong?.durationMs ?: 0)
if (newState != playerState) {
playerState = newState
notifyStateChanged()
}
}
@Synchronized
override fun startAction(action: InternalPlayer.Action) {
val internalPlayer = internalPlayer
if (internalPlayer == null || !internalPlayer.performAction(action)) {
logD("Internal player not present or did not consume action, waiting")
pendingAction = action
}
}
@Synchronized
override fun requestAction(internalPlayer: InternalPlayer) {
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
logW("Given internal player did not match current internal player")
return
}
if (pendingAction?.let(internalPlayer::performAction) == true) {
logD("Pending action consumed")
pendingAction = null
}
}
@Synchronized
override fun setPlaying(isPlaying: Boolean) {
internalPlayer?.setPlaying(isPlaying)
}
@Synchronized
override fun seekTo(positionMs: Long) {
internalPlayer?.seekTo(positionMs)
}
// --- PERSISTENCE FUNCTIONS ---
@Synchronized
override fun toSavedState() =
queue.toSavedState()?.let {
PlaybackStateManager.SavedState(
parent = parent,
queueState = it,
positionMs = playerState.calculateElapsedPositionMs(),
repeatMode = repeatMode)
}
@Synchronized
override fun applySavedState(
savedState: PlaybackStateManager.SavedState,
destructive: Boolean
) {
if (isInitialized && !destructive) {
return
}
val internalPlayer = internalPlayer ?: return
logD("Restoring state $savedState")
parent = savedState.parent
queue.applySavedState(savedState.queueState)
repeatMode = savedState.repeatMode
notifyNewPlayback()
// Continuing playback while also possibly doing drastic state updates is
// a bad idea, so pause.
internalPlayer.loadSong(queue.currentSong, false)
if (queue.currentSong != null) {
// Internal player may have reloaded the media item, re-seek to the previous position
seekTo(savedState.positionMs)
}
}
// --- CALLBACKS ---
private fun notifyIndexMoved() {
for (callback in listeners) {
callback.onIndexMoved(queue)
}
}
private fun notifyQueueChanged(change: Queue.ChangeResult) {
for (callback in listeners) {
callback.onQueueChanged(queue, change)
}
}
private fun notifyQueueReordered() {
for (callback in listeners) {
callback.onQueueReordered(queue)
}
}
private fun notifyNewPlayback() {
for (callback in listeners) {
callback.onNewPlayback(queue, parent)
}
}
private fun notifyStateChanged() {
for (callback in listeners) {
callback.onStateChanged(playerState)
}
}
private fun notifyRepeatModeChanged() {
for (callback in listeners) {
callback.onRepeatChanged(repeatMode)
}
}
}

View file

@ -30,7 +30,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
*/
class MediaButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val playbackManager = PlaybackStateManager.getInstance()
val playbackManager = PlaybackStateManager.get()
if (playbackManager.queue.currentSong != null) {
// We have a song, so we can assume that the service will start a foreground state.
// At least, I hope. Again, *this is why we don't do this*. I cannot describe how

View file

@ -59,7 +59,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
setQueueTitle(context.getString(R.string.lbl_queue))
}
private val playbackManager = PlaybackStateManager.getInstance()
private val playbackManager = PlaybackStateManager.get()
private val playbackSettings = PlaybackSettings.from(context)
private val notification = NotificationComponent(context, mediaSession.sessionToken)

View file

@ -91,7 +91,7 @@ class PlaybackService :
private val systemReceiver = PlaybackReceiver()
// Managers
private val playbackManager = PlaybackStateManager.getInstance()
private val playbackManager = PlaybackStateManager.get()
private val musicStore = MusicStore.getInstance()
private lateinit var musicSettings: MusicSettings
private lateinit var playbackSettings: PlaybackSettings
@ -333,7 +333,7 @@ class PlaybackService :
// 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 { playbackManager.saveState(persistenceRepository) }
saveScope.launch { persistenceRepository.saveState(playbackManager.toSavedState()) }
}
}
@ -348,7 +348,11 @@ class PlaybackService :
when (action) {
// Restore state -> Start a new restoreState job
is InternalPlayer.Action.RestoreState -> {
restoreScope.launch { playbackManager.restoreState(persistenceRepository, false) }
restoreScope.launch {
persistenceRepository.readState(library)?.let {
playbackManager.applySavedState(it, false)
}
}
}
// Shuffle all -> Start new playback from all songs
is InternalPlayer.Action.ShuffleAll -> {

View file

@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.logD
*/
class WidgetComponent(private val context: Context) :
PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener {
private val playbackManager = PlaybackStateManager.getInstance()
private val playbackManager = PlaybackStateManager.get()
private val uiSettings = UISettings.from(context)
private val imageSettings = ImageSettings.from(context)
private val widgetProvider = WidgetProvider()
@ -133,7 +133,7 @@ class WidgetComponent(private val context: Context) :
* @param cover A pre-loaded album cover [Bitmap] for [song].
* @param isPlaying [PlaybackStateManager.playerState]
* @param repeatMode [PlaybackStateManager.repeatMode]
* @param isShuffled [PlaybackStateManager.isShuffled]
* @param isShuffled [Queue.isShuffled]
*/
data class PlaybackState(
val song: Song,