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:
parent
c655f7d39e
commit
bb2ea9df27
14 changed files with 543 additions and 551 deletions
|
|
@ -68,25 +68,11 @@ class Library(rawSongs: List<Song.Raw>, settings: MusicSettings) {
|
||||||
fun sanitize(song: Song) = find<Song>(song.uid)
|
fun sanitize(song: Song) = find<Song>(song.uid)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a [Album] from an another library into a [Album] in this [Library].
|
* Convert a [MusicParent] from an another library into a [MusicParent] in this [Library].
|
||||||
* @param album The [Album] to convert.
|
* @param parent The [MusicParent] to convert.
|
||||||
* @return The analogous [Album] in this [Library], or null if it does not exist.
|
* @return The analogous [Album] in this [Library], or null if it does not exist.
|
||||||
*/
|
*/
|
||||||
fun sanitize(album: Album) = find<Album>(album.uid)
|
fun <T : MusicParent> sanitize(parent: T) = find<T>(parent.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)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri].
|
* Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri].
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
|
class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
|
||||||
private val indexer = Indexer.getInstance()
|
private val indexer = Indexer.getInstance()
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.get()
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
||||||
private var currentIndexJob: Job? = null
|
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
|
// Clear invalid models from PlaybackStateManager. This is not connected
|
||||||
// to a listener as it is bad practice for a shared object to attach to
|
// to a listener as it is bad practice for a shared object to attach to
|
||||||
// the listener system of another.
|
// 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.
|
// Forward the new library to MusicStore to continue the update process.
|
||||||
musicStore.library = newLibrary
|
musicStore.library = newLibrary
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
AndroidViewModel(application), PlaybackStateManager.Listener {
|
AndroidViewModel(application), PlaybackStateManager.Listener {
|
||||||
private val musicSettings = MusicSettings.from(application)
|
private val musicSettings = MusicSettings.from(application)
|
||||||
private val playbackSettings = PlaybackSettings.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 persistenceRepository = PersistenceRepository.from(application)
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private var lastPositionJob: Job? = null
|
private var lastPositionJob: Job? = null
|
||||||
|
|
@ -430,8 +430,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
*/
|
*/
|
||||||
fun savePlaybackState(onDone: (Boolean) -> Unit) {
|
fun savePlaybackState(onDone: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val saved = playbackManager.saveState(persistenceRepository)
|
onDone(persistenceRepository.saveState(playbackManager.toSavedState()))
|
||||||
onDone(saved)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,10 +439,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
* @param onDone Called when the wipe is completed with true if successful, and false otherwise.
|
* @param onDone Called when the wipe is completed with true if successful, and false otherwise.
|
||||||
*/
|
*/
|
||||||
fun wipePlaybackState(onDone: (Boolean) -> Unit) {
|
fun wipePlaybackState(onDone: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch { onDone(persistenceRepository.saveState(null)) }
|
||||||
val wiped = playbackManager.wipeState(persistenceRepository)
|
|
||||||
onDone(wiped)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -453,8 +449,16 @@ class PlaybackViewModel(application: Application) :
|
||||||
*/
|
*/
|
||||||
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
|
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val restored = playbackManager.restoreState(persistenceRepository, true)
|
val library = musicStore.library
|
||||||
onDone(restored)
|
if (library != null) {
|
||||||
|
val savedState = persistenceRepository.readState(library)
|
||||||
|
if (savedState != null) {
|
||||||
|
playbackManager.applySavedState(savedState, true)
|
||||||
|
onDone(true)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDone(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ abstract class PersistenceDatabase : RoomDatabase() {
|
||||||
PersistenceDatabase::class.java,
|
PersistenceDatabase::class.java,
|
||||||
"auxio_playback_persistence.db")
|
"auxio_playback_persistence.db")
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
|
.fallbackToDestructiveMigrationFrom(1)
|
||||||
.fallbackToDestructiveMigrationOnDowngrade()
|
.fallbackToDestructiveMigrationOnDowngrade()
|
||||||
.build()
|
.build()
|
||||||
INSTANCE = newInstance
|
INSTANCE = newInstance
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ import android.content.Context
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import org.oxycblt.auxio.music.library.Library
|
||||||
import org.oxycblt.auxio.playback.queue.Queue
|
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.logD
|
||||||
|
import org.oxycblt.auxio.util.logE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the persisted playback state in a structured manner.
|
* Manages the persisted playback state in a structured manner.
|
||||||
|
|
@ -30,30 +31,16 @@ import org.oxycblt.auxio.util.logD
|
||||||
*/
|
*/
|
||||||
interface PersistenceRepository {
|
interface PersistenceRepository {
|
||||||
/**
|
/**
|
||||||
* Read the previously persisted [SavedState].
|
* Read the previously persisted [PlaybackStateManager.SavedState].
|
||||||
* @param library The [Library] required to de-serialize the [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].
|
* Persist a new [PlaybackStateManager.SavedState].
|
||||||
* @param state The [SavedState] to persist.
|
* @param state The [PlaybackStateManager.SavedState] to persist.
|
||||||
*/
|
*/
|
||||||
suspend fun saveState(state: SavedState?)
|
suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,10 +56,19 @@ private class RealPersistenceRepository(private val context: Context) : Persiste
|
||||||
private val playbackStateDao: PlaybackStateDao by lazy { database.playbackStateDao() }
|
private val playbackStateDao: PlaybackStateDao by lazy { database.playbackStateDao() }
|
||||||
private val queueDao: QueueDao by lazy { database.queueDao() }
|
private val queueDao: QueueDao by lazy { database.queueDao() }
|
||||||
|
|
||||||
override suspend fun readState(library: Library): PersistenceRepository.SavedState? {
|
override suspend fun readState(library: Library): PlaybackStateManager.SavedState? {
|
||||||
val playbackState = playbackStateDao.getState() ?: return null
|
val playbackState: PlaybackState
|
||||||
val heap = queueDao.getHeap()
|
val heap: List<QueueHeapItem>
|
||||||
val mapping = queueDao.getMapping()
|
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 orderedMapping = mutableListOf<Int>()
|
||||||
val shuffledMapping = 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) }
|
val parent = playbackState.parentUid?.let { library.find<MusicParent>(it) }
|
||||||
|
logD("Read playback state")
|
||||||
|
|
||||||
return PersistenceRepository.SavedState(
|
return PlaybackStateManager.SavedState(
|
||||||
parent = parent,
|
parent = parent,
|
||||||
queueState =
|
queueState =
|
||||||
Queue.SavedState(
|
Queue.SavedState(
|
||||||
|
|
@ -96,12 +93,18 @@ private class RealPersistenceRepository(private val context: Context) : Persiste
|
||||||
repeatMode = playbackState.repeatMode)
|
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.
|
// Only bother saving a state if a song is actively playing from one.
|
||||||
// This is not the case with a null state.
|
// This is not the case with a null state.
|
||||||
playbackStateDao.nukeState()
|
try {
|
||||||
queueDao.nukeHeap()
|
playbackStateDao.nukeState()
|
||||||
queueDao.nukeMapping()
|
queueDao.nukeHeap()
|
||||||
|
queueDao.nukeMapping()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logE("Unable to clear previous state")
|
||||||
|
logE(e.stackTraceToString())
|
||||||
|
return false
|
||||||
|
}
|
||||||
logD("Cleared state")
|
logD("Cleared state")
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
// Transform saved state into raw state, which can then be written to the database.
|
// 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,
|
repeatMode = state.repeatMode,
|
||||||
songUid = state.queueState.songUid,
|
songUid = state.queueState.songUid,
|
||||||
parentUid = state.parent?.uid)
|
parentUid = state.parent?.uid)
|
||||||
playbackStateDao.insertState(playbackState)
|
|
||||||
|
|
||||||
// Convert the remaining queue information do their database-specific counterparts.
|
// Convert the remaining queue information do their database-specific counterparts.
|
||||||
val heap =
|
val heap =
|
||||||
state.queueState.heap.mapIndexed { i, song ->
|
state.queueState.heap.mapIndexed { i, song ->
|
||||||
QueueHeapItem(i, requireNotNull(song).uid)
|
QueueHeapItem(i, requireNotNull(song).uid)
|
||||||
}
|
}
|
||||||
queueDao.insertHeap(heap)
|
|
||||||
val mapping =
|
val mapping =
|
||||||
state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed {
|
state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed {
|
||||||
i,
|
i,
|
||||||
pair ->
|
pair ->
|
||||||
QueueMappingItem(i, pair.first, pair.second)
|
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")
|
logD("Wrote state")
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,30 +36,86 @@ import org.oxycblt.auxio.music.Song
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @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 heap = mutableListOf<Song>()
|
||||||
@Volatile private var orderedMapping = mutableListOf<Int>()
|
@Volatile private var orderedMapping = mutableListOf<Int>()
|
||||||
@Volatile private var shuffledMapping = mutableListOf<Int>()
|
@Volatile private var shuffledMapping = mutableListOf<Int>()
|
||||||
/** The index of the currently playing [Song] in the current mapping. */
|
/** The index of the currently playing [Song] in the current mapping. */
|
||||||
@Volatile
|
@Volatile
|
||||||
var index = -1
|
override var index = -1
|
||||||
private set
|
private set
|
||||||
/** The currently playing [Song]. */
|
/** The currently playing [Song]. */
|
||||||
val currentSong: Song?
|
override val currentSong: Song?
|
||||||
get() =
|
get() =
|
||||||
shuffledMapping
|
shuffledMapping
|
||||||
.ifEmpty { orderedMapping.ifEmpty { null } }
|
.ifEmpty { orderedMapping.ifEmpty { null } }
|
||||||
?.getOrNull(index)
|
?.getOrNull(index)
|
||||||
?.let(heap::get)
|
?.let(heap::get)
|
||||||
/** Whether this queue is shuffled. */
|
/** Whether this queue is shuffled. */
|
||||||
val isShuffled: Boolean
|
override val isShuffled: Boolean
|
||||||
get() = shuffledMapping.isNotEmpty()
|
get() = shuffledMapping.isNotEmpty()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve this queue into a more conventional list of [Song]s.
|
* Resolve this queue into a more conventional list of [Song]s.
|
||||||
* @return A list of [Song] corresponding to the current queue mapping.
|
* @return A list of [Song] corresponding to the current queue mapping.
|
||||||
*/
|
*/
|
||||||
fun resolve() =
|
override fun resolve() =
|
||||||
if (currentSong != null) {
|
if (currentSong != null) {
|
||||||
shuffledMapping.map { heap[it] }.ifEmpty { orderedMapping.map { heap[it] } }
|
shuffledMapping.map { heap[it] }.ifEmpty { orderedMapping.map { heap[it] } }
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -137,11 +193,11 @@ class Queue {
|
||||||
* @return [ChangeResult.MAPPING] if added to an existing queue, or [ChangeResult.SONG] if there
|
* @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.
|
* 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()) {
|
if (orderedMapping.isEmpty()) {
|
||||||
// No playback, start playing these songs.
|
// No playback, start playing these songs.
|
||||||
start(songs[0], songs, false)
|
start(songs[0], songs, false)
|
||||||
return ChangeResult.SONG
|
return Queue.ChangeResult.SONG
|
||||||
}
|
}
|
||||||
|
|
||||||
val heapIndices = songs.map(::addSongToHeap)
|
val heapIndices = songs.map(::addSongToHeap)
|
||||||
|
|
@ -156,20 +212,21 @@ class Queue {
|
||||||
orderedMapping.addAll(index + 1, heapIndices)
|
orderedMapping.addAll(index + 1, heapIndices)
|
||||||
}
|
}
|
||||||
check()
|
check()
|
||||||
return ChangeResult.MAPPING
|
return Queue.ChangeResult.MAPPING
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add [Song]s to the end of the queue. Will start playback if nothing is playing.
|
* Add [Song]s to the end of the queue. Will start playback if nothing is playing.
|
||||||
* @param songs The [Song]s to add.
|
* @param songs The [Song]s to add.
|
||||||
* @return [ChangeResult.MAPPING] if added to an existing queue, or [ChangeResult.SONG] if there
|
* @return [Queue.ChangeResult.MAPPING] if added to an existing queue, or
|
||||||
* was no prior playback and these enqueued [Song]s start new playback.
|
* [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()) {
|
if (orderedMapping.isEmpty()) {
|
||||||
// No playback, start playing these songs.
|
// No playback, start playing these songs.
|
||||||
start(songs[0], songs, false)
|
start(songs[0], songs, false)
|
||||||
return ChangeResult.SONG
|
return Queue.ChangeResult.SONG
|
||||||
}
|
}
|
||||||
|
|
||||||
val heapIndices = songs.map(::addSongToHeap)
|
val heapIndices = songs.map(::addSongToHeap)
|
||||||
|
|
@ -179,18 +236,18 @@ class Queue {
|
||||||
shuffledMapping.addAll(heapIndices)
|
shuffledMapping.addAll(heapIndices)
|
||||||
}
|
}
|
||||||
check()
|
check()
|
||||||
return ChangeResult.MAPPING
|
return Queue.ChangeResult.MAPPING
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move a [Song] at the given position to a new position.
|
* Move a [Song] at the given position to a new position.
|
||||||
* @param src The position of the [Song] to move.
|
* @param src The position of the [Song] to move.
|
||||||
* @param dst The destination position of the [Song].
|
* @param dst The destination position of the [Song].
|
||||||
* @return [ChangeResult.MAPPING] if the move occurred after the current index,
|
* @return [Queue.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
|
* [Queue.ChangeResult.INDEX] if the move occurred before or at the current index, requiring it
|
||||||
* mutated.
|
* to be mutated.
|
||||||
*/
|
*/
|
||||||
fun move(src: Int, dst: Int): ChangeResult {
|
fun move(src: Int, dst: Int): Queue.ChangeResult {
|
||||||
if (shuffledMapping.isNotEmpty()) {
|
if (shuffledMapping.isNotEmpty()) {
|
||||||
// Move songs only in the shuffled mapping. There is no sane analogous form of
|
// Move songs only in the shuffled mapping. There is no sane analogous form of
|
||||||
// this for the ordered mapping.
|
// this for the ordered mapping.
|
||||||
|
|
@ -210,21 +267,21 @@ class Queue {
|
||||||
else -> {
|
else -> {
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
check()
|
check()
|
||||||
return ChangeResult.MAPPING
|
return Queue.ChangeResult.MAPPING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
check()
|
check()
|
||||||
return ChangeResult.INDEX
|
return Queue.ChangeResult.INDEX
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a [Song] at the given position.
|
* Remove a [Song] at the given position.
|
||||||
* @param at The position of the [Song] to remove.
|
* @param at The position of the [Song] to remove.
|
||||||
* @return [ChangeResult.MAPPING] if the removed [Song] was after the current index,
|
* @return [Queue.ChangeResult.MAPPING] if the removed [Song] was after the current index,
|
||||||
* [ChangeResult.INDEX] if the removed [Song] was before the current index, and
|
* [Queue.ChangeResult.INDEX] if the removed [Song] was before the current index, and
|
||||||
* [ChangeResult.SONG] if the currently playing [Song] was removed.
|
* [Queue.ChangeResult.SONG] if the currently playing [Song] was removed.
|
||||||
*/
|
*/
|
||||||
fun remove(at: Int): ChangeResult {
|
fun remove(at: Int): Queue.ChangeResult {
|
||||||
if (shuffledMapping.isNotEmpty()) {
|
if (shuffledMapping.isNotEmpty()) {
|
||||||
// Remove the specified index in the shuffled mapping and the analogous song in the
|
// Remove the specified index in the shuffled mapping and the analogous song in the
|
||||||
// ordered mapping.
|
// ordered mapping.
|
||||||
|
|
@ -242,34 +299,34 @@ class Queue {
|
||||||
val result =
|
val result =
|
||||||
when {
|
when {
|
||||||
// We just removed the currently playing song.
|
// 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 was ahead of removed song, shift back to preserve consistency.
|
||||||
index > at -> {
|
index > at -> {
|
||||||
index -= 1
|
index -= 1
|
||||||
ChangeResult.INDEX
|
Queue.ChangeResult.INDEX
|
||||||
}
|
}
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
else -> ChangeResult.MAPPING
|
else -> Queue.ChangeResult.MAPPING
|
||||||
}
|
}
|
||||||
check()
|
check()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the current state of this instance into a [SavedState].
|
* Convert the current state of this instance into a [Queue.SavedState].
|
||||||
* @return A new [SavedState] reflecting the exact state of the queue when called.
|
* @return A new [Queue.SavedState] reflecting the exact state of the queue when called.
|
||||||
*/
|
*/
|
||||||
fun toSavedState() =
|
fun toSavedState() =
|
||||||
currentSong?.let { song ->
|
currentSong?.let { song ->
|
||||||
SavedState(
|
Queue.SavedState(
|
||||||
heap.toList(), orderedMapping.toList(), shuffledMapping.toList(), index, song.uid)
|
heap.toList(), orderedMapping.toList(), shuffledMapping.toList(), index, song.uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update this instance from the given [SavedState].
|
* Update this instance from the given [Queue.SavedState].
|
||||||
* @param savedState A [SavedState] with a valid queue representation.
|
* @param savedState A [Queue.SavedState] with a valid queue representation.
|
||||||
*/
|
*/
|
||||||
fun applySavedState(savedState: SavedState) {
|
fun applySavedState(savedState: Queue.SavedState) {
|
||||||
val adjustments = mutableListOf<Int?>()
|
val adjustments = mutableListOf<Int?>()
|
||||||
var currentShift = 0
|
var currentShift = 0
|
||||||
for (song in savedState.heap) {
|
for (song in savedState.heap) {
|
||||||
|
|
@ -345,49 +402,4 @@ class Queue {
|
||||||
"Queue inconsistency detected: Shuffled mapping indices out of heap bounds"
|
"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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
|
class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.get()
|
||||||
|
|
||||||
private val _queue = MutableStateFlow(listOf<Song>())
|
private val _queue = MutableStateFlow(listOf<Song>())
|
||||||
/** The current queue. */
|
/** The current queue. */
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
*/
|
*/
|
||||||
class ReplayGainAudioProcessor(context: Context) :
|
class ReplayGainAudioProcessor(context: Context) :
|
||||||
BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener {
|
BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener {
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.get()
|
||||||
private val playbackSettings = PlaybackSettings.from(context)
|
private val playbackSettings = PlaybackSettings.from(context)
|
||||||
private var lastFormat: Format? = null
|
private var lastFormat: Format? = null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -17,19 +17,13 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import org.oxycblt.auxio.playback.queue.EditableQueue
|
||||||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
|
||||||
import org.oxycblt.auxio.playback.queue.Queue
|
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.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core playback state controller class.
|
* 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.
|
* MediaSession is poorly designed. This class instead ful-fills this role.
|
||||||
*
|
*
|
||||||
* This should ***NOT*** be used outside of the playback module.
|
* This should ***NOT*** be used outside of the playback module.
|
||||||
* - If you want to use the playback state in the UI, use
|
* - If you want to use the playback state in the UI, use PlaybackViewModel as it can withstand
|
||||||
* [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs.
|
* volatile UIs.
|
||||||
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use
|
* - 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
|
* Internal consumers should usually use [Listener], however the component that manages the player
|
||||||
* itself should instead use [InternalPlayer].
|
* itself should instead use [InternalPlayer].
|
||||||
*
|
*
|
||||||
* All access should be done with [PlaybackStateManager.getInstance].
|
* All access should be done with [get].
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class PlaybackStateManager private constructor() {
|
interface PlaybackStateManager {
|
||||||
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
|
|
||||||
|
|
||||||
/** The current [Queue]. */
|
/** The current [Queue]. */
|
||||||
val queue = Queue()
|
val queue: Queue
|
||||||
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
||||||
@Volatile
|
val parent: MusicParent?
|
||||||
var parent: MusicParent? = null // FIXME: Parent is interpreted wrong when nothing is playing.
|
|
||||||
private set
|
|
||||||
|
|
||||||
/** The current [InternalPlayer] state. */
|
/** The current [InternalPlayer] state. */
|
||||||
@Volatile
|
val playerState: InternalPlayer.State
|
||||||
var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
|
|
||||||
private set
|
|
||||||
/** The current [RepeatMode] */
|
/** The current [RepeatMode] */
|
||||||
@Volatile
|
var repeatMode: RepeatMode
|
||||||
var repeatMode = RepeatMode.NONE
|
/** The audio session ID of the internal player. Null if no internal player exists. */
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
notifyRepeatModeChanged()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The current audio session ID of the internal player. Null if [InternalPlayer] is unavailable.
|
|
||||||
*/
|
|
||||||
val currentAudioSessionId: Int?
|
val currentAudioSessionId: Int?
|
||||||
get() = internalPlayer?.audioSessionId
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a [Listener] to this instance. This can be used to receive changes in the playback state.
|
* 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.
|
* @param listener The [Listener] to add.
|
||||||
* @see Listener
|
* @see Listener
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun addListener(listener: Listener)
|
||||||
fun addListener(listener: Listener) {
|
|
||||||
if (isInitialized) {
|
|
||||||
listener.onNewPlayback(queue, parent)
|
|
||||||
listener.onRepeatChanged(repeatMode)
|
|
||||||
listener.onStateChanged(playerState)
|
|
||||||
}
|
|
||||||
|
|
||||||
listeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a [Listener] from this instance, preventing it from receiving any further updates.
|
* Remove a [Listener] from this instance, preventing it from receiving any further updates.
|
||||||
|
|
@ -104,10 +70,7 @@ class PlaybackStateManager private constructor() {
|
||||||
* the first place.
|
* the first place.
|
||||||
* @see Listener
|
* @see Listener
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun removeListener(listener: Listener)
|
||||||
fun removeListener(listener: Listener) {
|
|
||||||
listeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an [InternalPlayer] for this instance. This instance will handle translating the
|
* 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
|
* @param internalPlayer The [InternalPlayer] to register. Will do nothing if already
|
||||||
* registered.
|
* registered.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun registerInternalPlayer(internalPlayer: InternalPlayer)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* commands.
|
||||||
* @param internalPlayer The [InternalPlayer] to unregister. Must be the current
|
* @param internalPlayer The [InternalPlayer] to unregister. Must be the current
|
||||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun unregisterInternalPlayer(internalPlayer: InternalPlayer)
|
||||||
fun unregisterInternalPlayer(internalPlayer: InternalPlayer) {
|
|
||||||
if (this.internalPlayer !== internalPlayer) {
|
|
||||||
logW("Given internal player did not match current internal player")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.internalPlayer = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- PLAYING FUNCTIONS ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start new playback.
|
* Start new playback.
|
||||||
|
|
@ -161,190 +97,81 @@ class PlaybackStateManager private constructor() {
|
||||||
* collection of "All [Song]s".
|
* collection of "All [Song]s".
|
||||||
* @param shuffled Whether to shuffle or not.
|
* @param shuffled Whether to shuffle or not.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean)
|
||||||
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 ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to the next [Song] in the queue. Will go to the first [Song] in the queue if there is no
|
* 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.
|
* [Song] ahead to skip to.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun next()
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to the previous [Song] in the queue. Will rewind if there are no previous [Song]s to skip
|
* 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.
|
* to, or if configured to do so.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun prev()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a [Song] at the given position in the queue.
|
* Play a [Song] at the given position in the queue.
|
||||||
* @param index The position of the [Song] in the queue to start playing.
|
* @param index The position of the [Song] in the queue to start playing.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun goto(index: Int)
|
||||||
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))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add [Song]s to the top of the queue.
|
* Add [Song]s to the top of the queue.
|
||||||
* @param songs The [Song]s to add.
|
* @param songs The [Song]s to add.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun playNext(songs: List<Song>)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a [Song] to the end of the queue.
|
* Add a [Song] to the top of the queue.
|
||||||
* @param song The [Song] to add.
|
* @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.
|
* Add [Song]s to the end of the queue.
|
||||||
* @param songs The [Song]s to add.
|
* @param songs The [Song]s to add.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun addToQueue(songs: List<Song>)
|
||||||
fun addToQueue(songs: List<Song>) {
|
|
||||||
val internalPlayer = internalPlayer ?: return
|
/**
|
||||||
when (queue.addToQueue(songs)) {
|
* Add a [Song] to the end of the queue.
|
||||||
Queue.ChangeResult.MAPPING -> notifyQueueChanged(Queue.ChangeResult.MAPPING)
|
* @param song The [Song] to add.
|
||||||
Queue.ChangeResult.SONG -> {
|
*/
|
||||||
// Enqueueing actually started a new playback session from all songs.
|
fun addToQueue(song: Song) = addToQueue(listOf(song))
|
||||||
parent = null
|
|
||||||
internalPlayer.loadSong(queue.currentSong, true)
|
|
||||||
notifyNewPlayback()
|
|
||||||
}
|
|
||||||
Queue.ChangeResult.INDEX -> error("Unreachable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move a [Song] in the queue.
|
* Move a [Song] in the queue.
|
||||||
* @param src The position of the [Song] to move in the queue.
|
* @param src The position of the [Song] to move in the queue.
|
||||||
* @param dst The destination position in the queue.
|
* @param dst The destination position in the queue.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun moveQueueItem(src: Int, dst: Int)
|
||||||
fun moveQueueItem(src: Int, dst: Int) {
|
|
||||||
logD("Moving item $src to position $dst")
|
|
||||||
notifyQueueChanged(queue.move(src, dst))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a [Song] from the queue.
|
* Remove a [Song] from the queue.
|
||||||
* @param at The position of the [Song] to remove in the queue.
|
* @param at The position of the [Song] to remove in the queue.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun removeQueueItem(at: Int)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (Re)shuffle or (Re)order this instance.
|
* (Re)shuffle or (Re)order this instance.
|
||||||
* @param shuffled Whether to shuffle the queue or not.
|
* @param shuffled Whether to shuffle the queue or not.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun reorder(shuffled: Boolean)
|
||||||
fun reorder(shuffled: Boolean) {
|
|
||||||
queue.reorder(shuffled)
|
|
||||||
notifyQueueReordered()
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- INTERNAL PLAYER FUNCTIONS ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronize the state of this instance with the current [InternalPlayer].
|
* Synchronize the state of this instance with the current [InternalPlayer].
|
||||||
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
||||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun synchronizeState(internalPlayer: InternalPlayer)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a [InternalPlayer.Action] for the current [InternalPlayer] to handle eventually.
|
* Start a [InternalPlayer.Action] for the current [InternalPlayer] to handle eventually.
|
||||||
* @param action The [InternalPlayer.Action] to perform.
|
* @param action The [InternalPlayer.Action] to perform.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun startAction(action: InternalPlayer.Action)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request that the pending [InternalPlayer.Action] (if any) be passed to the given
|
* 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
|
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
||||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun requestAction(internalPlayer: InternalPlayer)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update whether playback is ongoing or not.
|
* Update whether playback is ongoing or not.
|
||||||
* @param isPlaying Whether playback is ongoing or not.
|
* @param isPlaying Whether playback is ongoing or not.
|
||||||
*/
|
*/
|
||||||
fun setPlaying(isPlaying: Boolean) {
|
fun setPlaying(isPlaying: Boolean)
|
||||||
internalPlayer?.setPlaying(isPlaying)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seek to the given position in the currently playing [Song].
|
* Seek to the given position in the currently playing [Song].
|
||||||
* @param positionMs The position to seek to, in milliseconds.
|
* @param positionMs The position to seek to, in milliseconds.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun seekTo(positionMs: Long)
|
||||||
fun seekTo(positionMs: Long) {
|
|
||||||
internalPlayer?.seekTo(positionMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Rewind to the beginning of the currently playing [Song]. */
|
/** Rewind to the beginning of the currently playing [Song]. */
|
||||||
fun rewind() = seekTo(0)
|
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.
|
* Restores this instance from the given [SavedState].
|
||||||
* @param repository The [PersistenceRepository] to load from.
|
* @param savedState The [SavedState] to restore from.
|
||||||
* @param force Whether to do a restore regardless of any prior playback state.
|
* @param destructive Whether to disregard the prior playback state and overwrite it with this
|
||||||
* @return If the state was restored, false otherwise.
|
* [SavedState].
|
||||||
*/
|
*/
|
||||||
suspend fun restoreState(repository: PersistenceRepository, force: Boolean): Boolean {
|
fun applySavedState(savedState: SavedState, destructive: 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The interface for receiving updates from [PlaybackStateManager]. Add the listener to
|
* The interface for receiving updates from [PlaybackStateManager]. Add the listener to
|
||||||
|
|
@ -606,6 +257,20 @@ class PlaybackStateManager private constructor() {
|
||||||
fun onRepeatChanged(repeatMode: RepeatMode) {}
|
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 {
|
companion object {
|
||||||
@Volatile private var INSTANCE: PlaybackStateManager? = null
|
@Volatile private var INSTANCE: PlaybackStateManager? = null
|
||||||
|
|
||||||
|
|
@ -613,7 +278,7 @@ class PlaybackStateManager private constructor() {
|
||||||
* Get a singleton instance.
|
* Get a singleton instance.
|
||||||
* @return The (possibly newly-created) singleton instance.
|
* @return The (possibly newly-created) singleton instance.
|
||||||
*/
|
*/
|
||||||
fun getInstance(): PlaybackStateManager {
|
fun get(): PlaybackStateManager {
|
||||||
val currentInstance = INSTANCE
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
if (currentInstance != null) {
|
if (currentInstance != null) {
|
||||||
|
|
@ -621,10 +286,310 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
val newInstance = PlaybackStateManager()
|
val newInstance = RealPlaybackStateManager()
|
||||||
INSTANCE = newInstance
|
INSTANCE = newInstance
|
||||||
return 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
*/
|
*/
|
||||||
class MediaButtonReceiver : BroadcastReceiver() {
|
class MediaButtonReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val playbackManager = PlaybackStateManager.getInstance()
|
val playbackManager = PlaybackStateManager.get()
|
||||||
if (playbackManager.queue.currentSong != null) {
|
if (playbackManager.queue.currentSong != null) {
|
||||||
// We have a song, so we can assume that the service will start a foreground state.
|
// 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
|
// At least, I hope. Again, *this is why we don't do this*. I cannot describe how
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
|
||||||
setQueueTitle(context.getString(R.string.lbl_queue))
|
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 playbackSettings = PlaybackSettings.from(context)
|
||||||
|
|
||||||
private val notification = NotificationComponent(context, mediaSession.sessionToken)
|
private val notification = NotificationComponent(context, mediaSession.sessionToken)
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ class PlaybackService :
|
||||||
private val systemReceiver = PlaybackReceiver()
|
private val systemReceiver = PlaybackReceiver()
|
||||||
|
|
||||||
// Managers
|
// Managers
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.get()
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private lateinit var musicSettings: MusicSettings
|
private lateinit var musicSettings: MusicSettings
|
||||||
private lateinit var playbackSettings: PlaybackSettings
|
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
|
// to save the current state as it's not long until this service (and likely the whole
|
||||||
// app) is killed.
|
// app) is killed.
|
||||||
logD("Saving playback state")
|
logD("Saving playback state")
|
||||||
saveScope.launch { playbackManager.saveState(persistenceRepository) }
|
saveScope.launch { persistenceRepository.saveState(playbackManager.toSavedState()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,7 +348,11 @@ class PlaybackService :
|
||||||
when (action) {
|
when (action) {
|
||||||
// Restore state -> Start a new restoreState job
|
// Restore state -> Start a new restoreState job
|
||||||
is InternalPlayer.Action.RestoreState -> {
|
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
|
// Shuffle all -> Start new playback from all songs
|
||||||
is InternalPlayer.Action.ShuffleAll -> {
|
is InternalPlayer.Action.ShuffleAll -> {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
*/
|
*/
|
||||||
class WidgetComponent(private val context: Context) :
|
class WidgetComponent(private val context: Context) :
|
||||||
PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener {
|
PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener {
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.get()
|
||||||
private val uiSettings = UISettings.from(context)
|
private val uiSettings = UISettings.from(context)
|
||||||
private val imageSettings = ImageSettings.from(context)
|
private val imageSettings = ImageSettings.from(context)
|
||||||
private val widgetProvider = WidgetProvider()
|
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 cover A pre-loaded album cover [Bitmap] for [song].
|
||||||
* @param isPlaying [PlaybackStateManager.playerState]
|
* @param isPlaying [PlaybackStateManager.playerState]
|
||||||
* @param repeatMode [PlaybackStateManager.repeatMode]
|
* @param repeatMode [PlaybackStateManager.repeatMode]
|
||||||
* @param isShuffled [PlaybackStateManager.isShuffled]
|
* @param isShuffled [Queue.isShuffled]
|
||||||
*/
|
*/
|
||||||
data class PlaybackState(
|
data class PlaybackState(
|
||||||
val song: Song,
|
val song: Song,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue