playback: mirror state internally

Mirror the last playback state of the holder inside
PlaybackStateManager.

This is generally more efficient and will enable better handling of
when state holders attach and detach.
This commit is contained in:
Alexander Capehart 2024-01-09 15:04:32 -07:00
parent d5622895d0
commit 1d63ad5b7b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 436 additions and 802 deletions

View file

@ -12,6 +12,7 @@
#### What's Fixed #### What's Fixed
- Fixed a crash occuring if you navigated to the settings page from the playlist view - Fixed a crash occuring if you navigated to the settings page from the playlist view
and then back and then back
- Fixed music loading failing with an SQL error with certain music folder configurations
## 3.3.0 ## 3.3.0

View file

@ -37,8 +37,8 @@ import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.persist.PersistenceRepository
import org.oxycblt.auxio.playback.state.DeferredPlayback import org.oxycblt.auxio.playback.state.DeferredPlayback
import org.oxycblt.auxio.playback.state.PlaybackEvent
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.Event
@ -130,52 +130,56 @@ constructor(
playbackSettings.unregisterListener(this) playbackSettings.unregisterListener(this)
} }
override fun onPlaybackEvent(event: PlaybackEvent) { override fun onIndexMoved(index: Int) {
when (event) { logD("Index moved, updating current song")
is PlaybackEvent.IndexMoved -> { _song.value = playbackManager.currentSong
logD("Index moved, updating current song") }
_song.value = event.currentSong
} override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
is PlaybackEvent.QueueChanged -> { // Other types of queue changes preserve the current song.
// Other types of queue changes preserve the current song. if (change.type == QueueChange.Type.SONG) {
if (event.change.type == QueueChange.Type.SONG) { logD("Queue changed, updating current song")
logD("Queue changed, updating current song") _song.value = playbackManager.currentSong
_song.value = event.currentSong }
}
override fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {
logD("Queue completely changed, updating current song")
_isShuffled.value = isShuffled
}
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {
logD("New playback started, updating playback information")
_song.value = playbackManager.currentSong
_parent.value = parent
_isShuffled.value = isShuffled
}
override fun onProgressionChanged(progression: Progression) {
logD("Player state changed, starting new position polling")
_isPlaying.value = progression.isPlaying
// Still need to update the position now due to co-routine launch delays
_positionDs.value = progression.calculateElapsedPositionMs().msToDs()
// Replace the previous position co-routine with a new one that uses the new
// state information.
lastPositionJob?.cancel()
lastPositionJob =
viewModelScope.launch {
while (true) {
_positionDs.value = progression.calculateElapsedPositionMs().msToDs()
// Wait a deci-second for the next position tick.
delay(100)
} }
} }
is PlaybackEvent.QueueReordered -> { }
logD("Queue completely changed, updating current song")
_isShuffled.value = event.isShuffled override fun onRepeatModeChanged(repeatMode: RepeatMode) {
} _repeatMode.value = repeatMode
is PlaybackEvent.NewPlayback -> {
logD("New playback started, updating playback information")
_song.value = event.currentSong
_parent.value = event.parent
_isShuffled.value = event.isShuffled
}
is PlaybackEvent.ProgressionChanged -> {
logD("Progression changed, starting new position polling")
_isPlaying.value = event.progression.isPlaying
// Still need to update the position now due to co-routine launch delays
_positionDs.value = event.progression.calculateElapsedPositionMs().msToDs()
// Replace the previous position co-routine with a new one that uses the new
// state information.
lastPositionJob?.cancel()
lastPositionJob =
viewModelScope.launch {
while (true) {
_positionDs.value =
event.progression.calculateElapsedPositionMs().msToDs()
// Wait a deci-second for the next position tick.
delay(100)
}
}
}
is PlaybackEvent.RepeatModeChanged -> {
logD("Repeat mode changed, updating current mode")
_repeatMode.value = event.repeatMode
}
}
} }
override fun onBarActionChanged() { override fun onBarActionChanged() {
@ -579,7 +583,7 @@ constructor(
/** Toggle [isShuffled] (ex. from on to off) */ /** Toggle [isShuffled] (ex. from on to off) */
fun toggleShuffled() { fun toggleShuffled() {
logD("Toggling shuffled state") logD("Toggling shuffled state")
playbackManager.reorder(!playbackManager.isShuffled) playbackManager.shuffled(!playbackManager.isShuffled)
} }
/** /**

View file

@ -1,449 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* Queue.kt is part of Auxio.
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.queue
import kotlin.random.Random
import kotlin.random.nextInt
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
/**
* A heap-backed play queue.
*
* Whereas other queue implementations use a plain list, Auxio requires a more complicated data
* structure in order to implement features such as gapless playback in ExoPlayer. This queue
* implementation is instead based around an unorganized "heap" of [Song] instances, that are then
* interpreted into different queues depending on the current playback configuration.
*
* In general, the implementation details don't need to be known for this data structure to be used,
* except in special circumstances like [SavedState]. The functions exposed should be familiar for
* any typical play queue.
*
* @author OxygenCobalt
*/
interface Queue {
/** The index of the currently playing [Song] in the current mapping. */
val index: Int
/** The currently playing [Song]. */
val currentSong: Song?
/** Whether this queue is shuffled. */
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.
*
* @param type The [Type] of the change to the internal queue state.
* @param instructions The update done to the resolved queue list.
*/
data class Change(val type: Type, val instructions: UpdateInstructions) {
enum class Type {
/** 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 MutableQueue : Queue {
@Volatile private var heap = mutableListOf<Song>()
@Volatile private var orderedMapping = mutableListOf<Int>()
@Volatile private var shuffledMapping = mutableListOf<Int>()
@Volatile
override var index = -1
private set
override val currentSong: Song?
get() =
shuffledMapping
.ifEmpty { orderedMapping.ifEmpty { null } }
?.getOrNull(index)
?.let(heap::get)
override val isShuffled: Boolean
get() = shuffledMapping.isNotEmpty()
override fun resolve() =
if (currentSong != null) {
shuffledMapping.map { heap[it] }.ifEmpty { orderedMapping.map { heap[it] } }
} else {
// Queue doesn't exist, return saner data.
listOf()
}
/**
* Go to a particular index in the queue.
*
* @param to The index of the [Song] to start playing, in the current queue mapping.
* @return true if the queue jumped to that position, false otherwise.
*/
fun goto(to: Int): Boolean {
if (to !in orderedMapping.indices) {
return false
}
index = to
return true
}
/**
* Start a new queue configuration.
*
* @param play The [Song] to play, or null to start from a random position.
* @param queue The queue of [Song]s to play. Must contain [play]. This list will become the
* heap internally.
* @param shuffled Whether to shuffle the queue or not. This changes the interpretation of
* [queue].
*/
fun start(play: Song?, queue: List<Song>, shuffled: Boolean) {
heap = queue.toMutableList()
orderedMapping = MutableList(queue.size) { it }
shuffledMapping = mutableListOf()
index =
play?.let(queue::indexOf) ?: if (shuffled) Random.Default.nextInt(queue.indices) else 0
reorder(shuffled)
check()
}
/**
* Re-order the queue.
*
* @param shuffled Whether the queue should be shuffled or not.
*/
fun reorder(shuffled: Boolean) {
if (orderedMapping.isEmpty()) {
// Nothing to do.
return
}
logD("Reordering queue [shuffled=$shuffled]")
if (shuffled) {
val trueIndex =
if (shuffledMapping.isNotEmpty()) {
// Re-shuffling, song to preserve is in the shuffled mapping
shuffledMapping[index]
} else {
// First shuffle, song to preserve is in the ordered mapping
orderedMapping[index]
}
// Since we are re-shuffling existing songs, we use the previous mapping size
// instead of the total queue size.
shuffledMapping = orderedMapping.shuffled().toMutableList()
shuffledMapping.add(0, shuffledMapping.removeAt(shuffledMapping.indexOf(trueIndex)))
index = 0
} else if (shuffledMapping.isNotEmpty()) {
// Ordering queue, song to preserve is in the shuffled mapping.
index = orderedMapping.indexOf(shuffledMapping[index])
shuffledMapping = mutableListOf()
}
check()
}
/**
* Add [Song]s to the "top" of the queue (right next to the currently playing song). Will start
* playback if nothing is playing.
*
* @param songs The [Song]s to add.
* @return A [Queue.Change] instance that reflects the changes made.
*/
fun addToTop(songs: List<Song>): Queue.Change {
logD("Adding ${songs.size} songs to the front of the queue")
val insertAt = index + 1
val heapIndices = songs.map(::addSongToHeap)
if (shuffledMapping.isNotEmpty()) {
// Add the new songs in front of the current index in the shuffled mapping and in front
// of the analogous list song in the ordered mapping.
logD("Must append songs to shuffled mapping")
val orderedIndex = orderedMapping.indexOf(shuffledMapping[index])
orderedMapping.addAll(orderedIndex + 1, heapIndices)
shuffledMapping.addAll(insertAt, heapIndices)
} else {
// Add the new song in front of the current index in the ordered mapping.
logD("Only appending songs to ordered mapping")
orderedMapping.addAll(insertAt, heapIndices)
}
check()
return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size))
}
/**
* Add [Song]s to the end of the queue. Will start playback if nothing is playing.
*
* @param songs The [Song]s to add.
* @return A [Queue.Change] instance that reflects the changes made.
*/
fun addToBottom(songs: List<Song>): Queue.Change {
logD("Adding ${songs.size} songs to the back of the queue")
val insertAt = orderedMapping.size
val heapIndices = songs.map(::addSongToHeap)
// Can simple append the new songs to the end of both mappings.
orderedMapping.addAll(heapIndices)
if (shuffledMapping.isNotEmpty()) {
logD("Appending songs to shuffled mapping")
shuffledMapping.addAll(heapIndices)
}
check()
return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size))
}
/**
* 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 A [Queue.Change] instance that reflects the changes made.
*/
fun move(src: Int, dst: Int): Queue.Change {
if (shuffledMapping.isNotEmpty()) {
// Move songs only in the shuffled mapping. There is no sane analogous form of
// this for the ordered mapping.
shuffledMapping.add(dst, shuffledMapping.removeAt(src))
} else {
// Move songs in the ordered mapping.
orderedMapping.add(dst, orderedMapping.removeAt(src))
}
val oldIndex = index
when (index) {
// We are moving the currently playing song, correct the index to it's new position.
src -> {
logD("Moving current song, shifting index")
index = dst
}
// We have moved an song from behind the playing song to in front, shift back.
in (src + 1)..dst -> {
logD("Moving song from behind -> front, shift backwards")
index -= 1
}
// We have moved an song from in front of the playing song to behind, shift forward.
in dst until src -> {
logD("Moving song from front -> behind, shift forward")
index += 1
}
else -> {
// Nothing to do.
logD("Move preserved index")
check()
return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Move(src, dst))
}
}
logD("Move changed index: $oldIndex -> $index")
check()
return Queue.Change(Queue.Change.Type.INDEX, UpdateInstructions.Move(src, dst))
}
/**
* Remove a [Song] at the given position.
*
* @param at The position of the [Song] to remove.
* @return A [Queue.Change] instance that reflects the changes made.
*/
fun remove(at: Int): Queue.Change {
val lastIndex = orderedMapping.lastIndex
if (shuffledMapping.isNotEmpty()) {
// Remove the specified index in the shuffled mapping and the analogous song in the
// ordered mapping.
orderedMapping.removeAt(orderedMapping.indexOf(shuffledMapping[at]))
shuffledMapping.removeAt(at)
} else {
// Remove the specified index in the shuffled mapping
orderedMapping.removeAt(at)
}
// Note: We do not clear songs out from the heap, as that would require the backing data
// of the player to be completely invalidated. It's generally easier to not remove the
// song and retain player state consistency.
val type =
when {
// We just removed the currently playing song.
index == at -> {
logD("Removed current song")
if (lastIndex == index) {
logD("Current song at end of queue, shift back")
--index
}
Queue.Change.Type.SONG
}
// Index was ahead of removed song, shift back to preserve consistency.
index > at -> {
logD("Removed before current song, shift back")
--index
Queue.Change.Type.INDEX
}
// Nothing to do
else -> {
logD("Removal preserved index")
Queue.Change.Type.MAPPING
}
}
logD("Committing change of type $type")
check()
return Queue.Change(type, UpdateInstructions.Remove(at, 1))
}
/**
* 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 ->
Queue.SavedState(
heap.toList(), orderedMapping.toList(), shuffledMapping.toList(), index, song.uid)
}
/**
* Update this instance from the given [Queue.SavedState].
*
* @param savedState A [Queue.SavedState] with a valid queue representation.
*/
fun applySavedState(savedState: Queue.SavedState) {
val adjustments = mutableListOf<Int?>()
var currentShift = 0
for (song in savedState.heap) {
if (song != null) {
adjustments.add(currentShift)
} else {
adjustments.add(null)
currentShift -= 1
}
}
logD("Created adjustment mapping [max shift=$currentShift]")
heap = savedState.heap.filterNotNull().toMutableList()
orderedMapping =
savedState.orderedMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
adjustments[heapIndex]?.let { heapIndex + it }
}
shuffledMapping =
savedState.shuffledMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
adjustments[heapIndex]?.let { heapIndex + it }
}
// Make sure we re-align the index to point to the previously playing song.
index = savedState.index
while (currentSong?.uid != savedState.songUid && index > -1) {
index--
}
logD("Corrected index: ${savedState.index} -> $index")
check()
}
private fun addSongToHeap(song: Song): Int {
// We want to first try to see if there are any "orphaned" songs in the queue
// that we can re-use. This way, we can reduce the memory used up by songs that
// were previously removed from the queue.
val currentMapping = orderedMapping
if (orderedMapping.isNotEmpty()) {
// While we could iterate through the queue and then check the mapping, it's
// faster if we first check the queue for all instances of this song, and then
// do a exclusion of this set of indices with the current mapping in order to
// obtain the orphaned songs.
val orphanCandidates = mutableSetOf<Int>()
for (entry in heap.withIndex()) {
if (entry.value == song) {
orphanCandidates.add(entry.index)
}
}
logD("Found orphans: ${orphanCandidates.map { heap[it] }}")
orphanCandidates.removeAll(currentMapping.toSet())
if (orphanCandidates.isNotEmpty()) {
val orphan = orphanCandidates.first()
logD("Found an orphan that could be re-used: ${heap[orphan]}")
// There are orphaned songs, return the first one we find.
return orphan
}
}
// Nothing to re-use, add this song to the queue
logD("No orphan could be re-used")
heap.add(song)
return heap.lastIndex
}
private fun check() {
check(!(heap.isEmpty() && (orderedMapping.isNotEmpty() || shuffledMapping.isNotEmpty()))) {
"Queue inconsistency detected: Empty heap with non-empty mappings" +
"[ordered: ${orderedMapping.size}, shuffled: ${shuffledMapping.size}]"
}
check(shuffledMapping.isEmpty() || orderedMapping.size == shuffledMapping.size) {
"Queue inconsistency detected: Ordered mapping size ${orderedMapping.size} " +
"!= Shuffled mapping size ${shuffledMapping.size}"
}
check(orderedMapping.all { it in heap.indices }) {
"Queue inconsistency detected: Ordered mapping indices out of heap bounds"
}
check(shuffledMapping.all { it in heap.indices }) {
"Queue inconsistency detected: Shuffled mapping indices out of heap bounds"
}
}
}

View file

@ -24,8 +24,8 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackEvent
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.Event
@ -52,7 +52,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
val scrollTo: Event<Int> val scrollTo: Event<Int>
get() = _scrollTo get() = _scrollTo
private val _index = MutableStateFlow(playbackManager.resolveQueue().index) private val _index = MutableStateFlow(playbackManager.index)
/** The index of the currently playing song in the queue. */ /** The index of the currently playing song in the queue. */
val index: StateFlow<Int> val index: StateFlow<Int>
get() = _index get() = _index
@ -61,45 +61,47 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
playbackManager.addListener(this) playbackManager.addListener(this)
} }
override fun onPlaybackEvent(event: PlaybackEvent) { override fun onIndexMoved(index: Int) {
when (event) { logD("Index moved, synchronizing and scrolling to new position")
is PlaybackEvent.IndexMoved -> { _scrollTo.put(index)
logD("Index moved, synchronizing and scrolling to new position") _index.value = index
_scrollTo.put(event.index) }
_index.value = event.index
} override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
is PlaybackEvent.QueueChanged -> { // Queue changed trivially due to item mo -> Diff queue, stay at current index.
// Queue changed trivially due to item mo -> Diff queue, stay at current index. logD("Updating queue display")
logD("Updating queue display") _queueInstructions.put(change.instructions)
_queueInstructions.put(event.change.instructions) _queue.value = queue
_queue.value = event.queue.queue if (change.type != QueueChange.Type.MAPPING) {
if (event.change.type != QueueChange.Type.MAPPING) { // Index changed, make sure it remains updated without actually scrolling to it.
// Index changed, make sure it remains updated without actually scrolling to it. logD("Index changed with queue, synchronizing new position")
logD("Index changed with queue, synchronizing new position") _index.value = index
_index.value = event.queue.index
}
}
is PlaybackEvent.QueueReordered -> {
// Queue changed completely -> Replace queue, update index
logD("Queue changed completely, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(event.queue.index)
_queue.value = event.queue.queue
_index.value = event.queue.index
}
is PlaybackEvent.NewPlayback -> {
// Entirely new queue -> Replace queue, update index
logD("New playback, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(event.queue.index)
_queue.value = event.queue.queue
_index.value = event.queue.index
}
is PlaybackEvent.RepeatModeChanged,
is PlaybackEvent.ProgressionChanged -> {}
} }
} }
override fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {
// Queue changed completely -> Replace queue, update index
logD("Queue changed completely, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(index)
_queue.value = queue
_index.value = index
}
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {
// Entirely new queue -> Replace queue, update index
logD("New playback, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(index)
_queue.value = queue
_index.value = index
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
playbackManager.removeListener(this) playbackManager.removeListener(this)

View file

@ -27,9 +27,9 @@ import java.nio.ByteBuffer
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.pow import kotlin.math.pow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.state.PlaybackEvent
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -70,32 +70,28 @@ constructor(
// --- OVERRIDES --- // --- OVERRIDES ---
override fun onPlaybackEvent(event: PlaybackEvent) { override fun onIndexMoved(index: Int) {
when (event) { logD("Index moved, updating current song")
is PlaybackEvent.IndexMoved -> { applyReplayGain(playbackManager.currentSong)
logD("Index moved, updating current song") }
applyReplayGain(event.currentSong)
} override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
is PlaybackEvent.QueueChanged -> { // Other types of queue changes preserve the current song.
// Queue changed trivially due to item mo -> Diff queue, stay at current index. if (change.type == QueueChange.Type.SONG) {
logD("Updating queue display") applyReplayGain(playbackManager.currentSong)
// Other types of queue changes preserve the current song.
if (event.change.type == QueueChange.Type.SONG) {
applyReplayGain(event.currentSong)
}
}
is PlaybackEvent.NewPlayback -> {
logD("New playback started, updating playback information")
applyReplayGain(event.currentSong)
}
is PlaybackEvent.ProgressionChanged,
is PlaybackEvent.QueueReordered,
is PlaybackEvent.RepeatModeChanged -> {
// Nothing to do
}
} }
} }
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {
logD("New playback started, updating playback information")
applyReplayGain(playbackManager.currentSong)
}
override fun onReplayGainSettingsChanged() { override fun onReplayGainSettingsChanged() {
// ReplayGain config changed, we need to set it up again. // ReplayGain config changed, we need to set it up again.
applyReplayGain(playbackManager.currentSong) applyReplayGain(playbackManager.currentSong)

View file

@ -27,19 +27,19 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
interface PlaybackStateHolder { interface PlaybackStateHolder {
val currentSong: Song? val progression: Progression
val repeatMode: RepeatMode val repeatMode: RepeatMode
val progression: Progression
val audioSessionId: Int
val parent: MusicParent? val parent: MusicParent?
fun resolveQueue(): List<Song>
fun resolveIndex(): Int
val isShuffled: Boolean val isShuffled: Boolean
fun resolveQueue(): Queue val audioSessionId: Int
fun newPlayback( fun newPlayback(
queue: List<Song>, queue: List<Song>,
@ -74,6 +74,21 @@ interface PlaybackStateHolder {
fun handleDeferred(action: DeferredPlayback): Boolean fun handleDeferred(action: DeferredPlayback): Boolean
} }
sealed interface StateEvent {
data object IndexMoved : StateEvent
data class QueueChanged(val instructions: UpdateInstructions, val songChanged: Boolean) :
StateEvent
data object QueueReordered : StateEvent
data object NewPlayback : StateEvent
data object ProgressionChanged : StateEvent
data object RepeatModeChanged : StateEvent
}
/** /**
* Represents the possible changes that can occur during certain queue mutation events. * Represents the possible changes that can occur during certain queue mutation events.
* *
@ -114,9 +129,9 @@ sealed interface DeferredPlayback {
data class Open(val uri: Uri) : DeferredPlayback data class Open(val uri: Uri) : DeferredPlayback
} }
data class Queue(val index: Int, val queue: List<Song>) { data class Queue(val songs: List<Song>, val index: Int) {
companion object { companion object {
fun nil() = Queue(-1, emptyList()) fun nil() = Queue(emptyList(), -1)
} }
} }

View file

@ -46,17 +46,17 @@ interface PlaybackStateManager {
/** The current [Progression] state. */ /** The current [Progression] state. */
val progression: Progression val progression: Progression
val currentSong: Song?
val repeatMode: RepeatMode val repeatMode: RepeatMode
val audioSessionId: Int
val parent: MusicParent? val parent: MusicParent?
val isShuffled: Boolean val currentSong: Song?
fun resolveQueue(): Queue val queue: List<Song>
val index: Int
val isShuffled: Boolean
/** The audio session ID of the internal player. Null if no internal player exists. */ /** The audio session ID of the internal player. Null if no internal player exists. */
val currentAudioSessionId: Int? val currentAudioSessionId: Int?
@ -178,9 +178,9 @@ interface PlaybackStateManager {
* *
* @param shuffled Whether to shuffle the queue or not. * @param shuffled Whether to shuffle the queue or not.
*/ */
fun reorder(shuffled: Boolean) fun shuffled(shuffled: Boolean)
fun dispatchEvent(stateHolder: PlaybackStateHolder, vararg events: PlaybackEvent) fun dispatchEvent(stateHolder: PlaybackStateHolder, event: StateEvent)
/** /**
* Start a [DeferredPlayback] for the current [PlaybackStateHolder] to handle eventually. * Start a [DeferredPlayback] for the current [PlaybackStateHolder] to handle eventually.
@ -240,7 +240,54 @@ interface PlaybackStateManager {
* [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener]. * [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener].
*/ */
interface Listener { interface Listener {
fun onPlaybackEvent(event: PlaybackEvent) /**
* Called when the position of the currently playing item has changed, changing the current
* [Song], but no other queue attribute has changed.
*/
fun onIndexMoved(index: Int) {}
/**
* Called when the [Queue] changed in a manner outlined by the given [Queue.Change].
*
* @param queue The new [Queue].
* @param change The type of [Queue.Change] that occurred.
*/
fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {}
/**
* Called when the [Queue] has changed in a non-trivial manner (such as re-shuffling), but
* the currently playing [Song] has not.
*
* @param queue The new [Queue].
*/
fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {}
/**
* Called when a new playback configuration was created.
*
* @param queue The new [Queue].
* @param parent The new [MusicParent] being played from, or null if playing from all songs.
*/
fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {}
/**
* Called when the state of the [InternalPlayer] changes.
*
* @param progression The new state of the [InternalPlayer].
*/
fun onProgressionChanged(progression: Progression) {}
/**
* Called when the [RepeatMode] changes.
*
* @param repeatMode The new [RepeatMode].
*/
fun onRepeatModeChanged(repeatMode: RepeatMode) {}
} }
/** /**
@ -259,52 +306,52 @@ interface PlaybackStateManager {
) )
} }
sealed interface PlaybackEvent {
class IndexMoved(val currentSong: Song?, val index: Int) : PlaybackEvent
class QueueChanged(val currentSong: Song?, val queue: Queue, val change: QueueChange) :
PlaybackEvent
class QueueReordered(val queue: Queue, val isShuffled: Boolean) : PlaybackEvent
class NewPlayback(
val currentSong: Song,
val queue: Queue,
val parent: MusicParent?,
val isShuffled: Boolean,
) : PlaybackEvent
class ProgressionChanged(val progression: Progression) : PlaybackEvent
class RepeatModeChanged(val repeatMode: RepeatMode) : PlaybackEvent
}
class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
private data class StateMirror(
val progression: Progression,
val repeatMode: RepeatMode,
val parent: MusicParent?,
val queue: List<Song>,
val index: Int,
val isShuffled: Boolean,
)
private val listeners = mutableListOf<PlaybackStateManager.Listener>() private val listeners = mutableListOf<PlaybackStateManager.Listener>()
@Volatile
private var stateMirror =
StateMirror(
progression = Progression.nil(),
repeatMode = RepeatMode.NONE,
parent = null,
queue = emptyList(),
index = -1,
isShuffled = false,
)
@Volatile private var stateHolder: PlaybackStateHolder? = null @Volatile private var stateHolder: PlaybackStateHolder? = null
@Volatile private var pendingDeferredPlayback: DeferredPlayback? = null @Volatile private var pendingDeferredPlayback: DeferredPlayback? = null
@Volatile private var isInitialized = false @Volatile private var isInitialized = false
/** The current [Progression] state. */ override val progression
override val progression: Progression get() = stateMirror.progression
get() = stateHolder?.progression ?: Progression.nil()
override val currentSong: Song? override val repeatMode
get() = stateHolder?.currentSong get() = stateMirror.repeatMode
override val repeatMode: RepeatMode override val parent
get() = stateHolder?.repeatMode ?: RepeatMode.NONE get() = stateMirror.parent
override val audioSessionId: Int override val currentSong
get() = stateHolder?.audioSessionId ?: -1 get() = stateMirror.queue.getOrNull(stateMirror.index)
override val parent: MusicParent? override val queue
get() = stateHolder?.parent get() = stateMirror.queue
override val isShuffled: Boolean override val index
get() = stateHolder?.isShuffled ?: false get() = stateMirror.index
override fun resolveQueue() = stateHolder?.resolveQueue() ?: Queue.nil() override val isShuffled
get() = stateMirror.isShuffled
override val currentAudioSessionId: Int? override val currentAudioSessionId: Int?
get() = stateHolder?.audioSessionId get() = stateHolder?.audioSessionId
@ -313,6 +360,14 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
override fun addListener(listener: PlaybackStateManager.Listener) { override fun addListener(listener: PlaybackStateManager.Listener) {
logD("Adding $listener to listeners") logD("Adding $listener to listeners")
listeners.add(listener) listeners.add(listener)
if (isInitialized) {
logD("Sending initial state to $listener")
listener.onNewPlayback(
stateMirror.parent, stateMirror.queue, stateMirror.index, stateMirror.isShuffled)
listener.onProgressionChanged(stateMirror.progression)
listener.onRepeatModeChanged(stateMirror.repeatMode)
}
} }
@Synchronized @Synchronized
@ -418,7 +473,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
} }
@Synchronized @Synchronized
override fun reorder(shuffled: Boolean) { override fun shuffled(shuffled: Boolean) {
val stateHolder = stateHolder ?: return val stateHolder = stateHolder ?: return
logD("Reordering queue [shuffled=$shuffled]") logD("Reordering queue [shuffled=$shuffled]")
stateHolder.reorder(shuffled) stateHolder.reorder(shuffled)
@ -470,15 +525,79 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
} }
@Synchronized @Synchronized
override fun dispatchEvent(stateHolder: PlaybackStateHolder, vararg events: PlaybackEvent) { override fun dispatchEvent(stateHolder: PlaybackStateHolder, event: StateEvent) {
if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) { if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) {
logW("Given internal player did not match current internal player") logW("Given internal player did not match current internal player")
return return
} }
events.forEach { event -> when (event) {
logD("Dispatching event $event") is StateEvent.IndexMoved -> {
listeners.forEach { it.onPlaybackEvent(event) } stateMirror =
stateMirror.copy(
index = stateHolder.resolveIndex(),
)
listeners.forEach { it.onIndexMoved(stateMirror.index) }
}
is StateEvent.QueueChanged -> {
val instructions = event.instructions
val newIndex = stateHolder.resolveIndex()
val changeType =
when {
event.songChanged -> {
QueueChange.Type.SONG
}
stateMirror.index != newIndex -> QueueChange.Type.INDEX
else -> QueueChange.Type.MAPPING
}
stateMirror = stateMirror.copy(queue = stateHolder.resolveQueue(), index = newIndex)
val change = QueueChange(changeType, instructions)
listeners.forEach {
it.onQueueChanged(stateMirror.queue, stateMirror.index, change)
}
}
is StateEvent.QueueReordered -> {
stateMirror =
stateMirror.copy(
queue = stateHolder.resolveQueue(),
index = stateHolder.resolveIndex(),
isShuffled = stateHolder.isShuffled,
)
listeners.forEach {
it.onQueueReordered(
stateMirror.queue, stateMirror.index, stateMirror.isShuffled)
}
}
is StateEvent.NewPlayback -> {
stateMirror =
stateMirror.copy(
parent = stateHolder.parent,
queue = stateHolder.resolveQueue(),
index = stateHolder.resolveIndex(),
isShuffled = stateHolder.isShuffled,
)
listeners.forEach {
it.onNewPlayback(
stateMirror.parent,
stateMirror.queue,
stateMirror.index,
stateMirror.isShuffled)
}
}
is StateEvent.ProgressionChanged -> {
stateMirror =
stateMirror.copy(
progression = stateHolder.progression,
)
listeners.forEach { it.onProgressionChanged(stateMirror.progression) }
}
is StateEvent.RepeatModeChanged -> {
stateMirror =
stateMirror.copy(
repeatMode = stateHolder.repeatMode,
)
listeners.forEach { it.onRepeatModeChanged(stateMirror.repeatMode) }
}
} }
} }

View file

@ -29,6 +29,19 @@ import org.oxycblt.auxio.util.logD
val ExoPlayer.song val ExoPlayer.song
get() = currentMediaItem?.song get() = currentMediaItem?.song
fun ExoPlayer.resolveIndex() = unscrambleQueueIndices().indexOf(currentMediaItemIndex)
fun ExoPlayer.resolveQueue() = unscrambleQueueIndices().map { getMediaItemAt(it).song }
val ExoPlayer.repeat: RepeatMode
get() =
when (repeatMode) {
Player.REPEAT_MODE_OFF -> RepeatMode.NONE
Player.REPEAT_MODE_ONE -> RepeatMode.TRACK
Player.REPEAT_MODE_ALL -> RepeatMode.ALL
else -> throw IllegalStateException("Unknown repeat mode: $repeatMode")
}
fun ExoPlayer.orderedQueue(queue: Collection<Song>, start: Song?) { fun ExoPlayer.orderedQueue(queue: Collection<Song>, start: Song?) {
clearMediaItems() clearMediaItems()
shuffleModeEnabled = false shuffleModeEnabled = false
@ -60,25 +73,6 @@ fun ExoPlayer.shuffledQueue(queue: Collection<Song>, start: Song?) {
seekTo(currentTimeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET) seekTo(currentTimeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET)
} }
val ExoPlayer.currentIndex: Int
get() {
val queue = unscrambleQueue { index -> index }
if (queue.isEmpty()) {
return C.INDEX_UNSET
}
return queue.indexOf(currentMediaItemIndex)
}
val ExoPlayer.repeat: RepeatMode
get() =
when (repeatMode) {
Player.REPEAT_MODE_OFF -> RepeatMode.NONE
Player.REPEAT_MODE_ONE -> RepeatMode.TRACK
Player.REPEAT_MODE_ALL -> RepeatMode.ALL
else -> throw IllegalStateException("Unknown repeat mode: $repeatMode")
}
fun ExoPlayer.reorder(shuffled: Boolean) { fun ExoPlayer.reorder(shuffled: Boolean) {
logD("Reordering queue to $shuffled") logD("Reordering queue to $shuffled")
shuffleModeEnabled = shuffled shuffleModeEnabled = shuffled
@ -97,23 +91,23 @@ fun ExoPlayer.addToQueue(songs: List<Song>) {
} }
fun ExoPlayer.goto(index: Int) { fun ExoPlayer.goto(index: Int) {
val queue = unscrambleQueue { index -> index } val indices = unscrambleQueueIndices()
if (queue.isEmpty()) { if (indices.isEmpty()) {
return return
} }
val trueIndex = queue[index] val trueIndex = indices[index]
seekTo(trueIndex, C.TIME_UNSET) seekTo(trueIndex, C.TIME_UNSET)
} }
fun ExoPlayer.move(from: Int, to: Int) { fun ExoPlayer.move(from: Int, to: Int) {
val queue = unscrambleQueue { index -> index } val indices = unscrambleQueueIndices()
if (queue.isEmpty()) { if (indices.isEmpty()) {
return return
} }
val trueFrom = queue[from] val trueFrom = indices[from]
val trueTo = queue[to] val trueTo = indices[to]
when { when {
trueFrom > trueTo -> { trueFrom > trueTo -> {
@ -128,29 +122,25 @@ fun ExoPlayer.move(from: Int, to: Int) {
} }
fun ExoPlayer.remove(at: Int) { fun ExoPlayer.remove(at: Int) {
val queue = unscrambleQueue { index -> index } val indices = unscrambleQueueIndices()
if (queue.isEmpty()) { if (indices.isEmpty()) {
return return
} }
val trueIndex = queue[at] val trueIndex = indices[at]
removeMediaItem(trueIndex) removeMediaItem(trueIndex)
} }
fun ExoPlayer.resolveQueue(): List<Song> { fun ExoPlayer.unscrambleQueueIndices(): List<Int> {
return unscrambleQueue { index -> getMediaItemAt(index).song }
}
inline fun <T> ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List<T> {
val timeline = currentTimeline val timeline = currentTimeline
if (timeline.isEmpty()) { if (timeline.isEmpty()) {
return emptyList() return emptyList()
} }
val queue = mutableListOf<T>() val queue = mutableListOf<Int>()
// Add the active queue item. // Add the active queue item.
val currentMediaItemIndex = currentMediaItemIndex val currentMediaItemIndex = currentMediaItemIndex
queue.add(mapper(currentMediaItemIndex)) queue.add(currentMediaItemIndex)
// Fill queue alternating with next and/or previous queue items. // Fill queue alternating with next and/or previous queue items.
var firstMediaItemIndex = currentMediaItemIndex var firstMediaItemIndex = currentMediaItemIndex
@ -164,7 +154,7 @@ inline fun <T> ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List<T> {
timeline.getNextWindowIndex( timeline.getNextWindowIndex(
lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled) lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
if (lastMediaItemIndex != C.INDEX_UNSET) { if (lastMediaItemIndex != C.INDEX_UNSET) {
queue.add(mapper(lastMediaItemIndex)) queue.add(lastMediaItemIndex)
} }
} }
if (firstMediaItemIndex != C.INDEX_UNSET) { if (firstMediaItemIndex != C.INDEX_UNSET) {
@ -172,7 +162,7 @@ inline fun <T> ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List<T> {
timeline.getPreviousWindowIndex( timeline.getPreviousWindowIndex(
firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled) firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
if (firstMediaItemIndex != C.INDEX_UNSET) { if (firstMediaItemIndex != C.INDEX_UNSET) {
queue.add(0, mapper(firstMediaItemIndex)) queue.add(0, firstMediaItemIndex)
} }
} }
} }

View file

@ -38,9 +38,8 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.ActionMode import org.oxycblt.auxio.playback.ActionMode
import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.state.PlaybackEvent
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Queue import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -118,60 +117,66 @@ constructor(
// --- PLAYBACKSTATEMANAGER OVERRIDES --- // --- PLAYBACKSTATEMANAGER OVERRIDES ---
override fun onPlaybackEvent(event: PlaybackEvent) { override fun onIndexMoved(index: Int) {
when (event) { updateMediaMetadata(playbackManager.currentSong, playbackManager.parent)
is PlaybackEvent.IndexMoved -> { invalidateSessionState()
updateMediaMetadata(event.currentSong, playbackManager.parent) }
invalidateSessionState()
}
is PlaybackEvent.QueueChanged -> {
updateQueue(event.queue)
when (event.change.type) {
// Nothing special to do with mapping changes.
QueueChange.Type.MAPPING -> {}
// Index changed, ensure playback state's index changes.
QueueChange.Type.INDEX -> invalidateSessionState()
// Song changed, ensure metadata changes.
QueueChange.Type.SONG ->
updateMediaMetadata(event.currentSong, playbackManager.parent)
}
}
is PlaybackEvent.QueueReordered -> {
updateQueue(event.queue)
invalidateSessionState()
mediaSession.setShuffleMode(
if (event.isShuffled) {
PlaybackStateCompat.SHUFFLE_MODE_ALL
} else {
PlaybackStateCompat.SHUFFLE_MODE_NONE
})
invalidateSecondaryAction()
}
is PlaybackEvent.NewPlayback -> {
updateMediaMetadata(event.currentSong, event.parent)
updateQueue(event.queue)
invalidateSessionState()
}
is PlaybackEvent.ProgressionChanged -> {
invalidateSessionState()
notification.updatePlaying(playbackManager.progression.isPlaying)
if (!bitmapProvider.isBusy) {
listener?.onPostNotification(notification)
}
}
is PlaybackEvent.RepeatModeChanged -> {
mediaSession.setRepeatMode(
when (event.repeatMode) {
RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE
RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
})
invalidateSecondaryAction() override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
} updateQueue(queue)
when (change.type) {
// Nothing special to do with mapping changes.
QueueChange.Type.MAPPING -> {}
// Index changed, ensure playback state's index changes.
QueueChange.Type.INDEX -> invalidateSessionState()
// Song changed, ensure metadata changes.
QueueChange.Type.SONG ->
updateMediaMetadata(playbackManager.currentSong, playbackManager.parent)
} }
} }
override fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {
updateQueue(queue)
invalidateSessionState()
mediaSession.setShuffleMode(
if (isShuffled) {
PlaybackStateCompat.SHUFFLE_MODE_ALL
} else {
PlaybackStateCompat.SHUFFLE_MODE_NONE
})
invalidateSecondaryAction()
}
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {
updateMediaMetadata(playbackManager.currentSong, parent)
updateQueue(queue)
invalidateSessionState()
}
override fun onProgressionChanged(progression: Progression) {
invalidateSessionState()
notification.updatePlaying(playbackManager.progression.isPlaying)
if (!bitmapProvider.isBusy) {
listener?.onPostNotification(notification)
}
}
override fun onRepeatModeChanged(repeatMode: RepeatMode) {
mediaSession.setRepeatMode(
when (repeatMode) {
RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE
RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
})
invalidateSecondaryAction()
}
// --- SETTINGS OVERRIDES --- // --- SETTINGS OVERRIDES ---
override fun onImageSettingsChanged() { override fun onImageSettingsChanged() {
@ -251,7 +256,7 @@ constructor(
} }
override fun onSetShuffleMode(shuffleMode: Int) { override fun onSetShuffleMode(shuffleMode: Int) {
playbackManager.reorder( playbackManager.shuffled(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP) shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP)
} }
@ -355,9 +360,9 @@ constructor(
* *
* @param queue The current queue to upload. * @param queue The current queue to upload.
*/ */
private fun updateQueue(queue: Queue) { private fun updateQueue(queue: List<Song>) {
val queueItems = val queueItems =
queue.queue.mapIndexed { i, song -> queue.mapIndexed { i, song ->
val description = val description =
MediaDescriptionCompat.Builder() MediaDescriptionCompat.Builder()
// Media ID should not be the item index but rather the UID, // Media ID should not be the item index but rather the UID,
@ -382,15 +387,13 @@ constructor(
private fun invalidateSessionState() { private fun invalidateSessionState() {
logD("Updating media session playback state") logD("Updating media session playback state")
val queue = playbackManager.resolveQueue()
val state = val state =
// InternalPlayer.State handles position/state information. // InternalPlayer.State handles position/state information.
playbackManager.progression playbackManager.progression
.intoPlaybackState(PlaybackStateCompat.Builder()) .intoPlaybackState(PlaybackStateCompat.Builder())
.setActions(ACTIONS) .setActions(ACTIONS)
// Active queue ID corresponds to the indices we populated prior, use them here. // Active queue ID corresponds to the indices we populated prior, use them here.
.setActiveQueueItemId(queue.index.toLong()) .setActiveQueueItemId(playbackManager.index.toLong())
// Android 13+ relies on custom actions in the notification. // Android 13+ relies on custom actions in the notification.

View file

@ -56,13 +56,11 @@ import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.persist.PersistenceRepository
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
import org.oxycblt.auxio.playback.state.DeferredPlayback import org.oxycblt.auxio.playback.state.DeferredPlayback
import org.oxycblt.auxio.playback.state.PlaybackEvent
import org.oxycblt.auxio.playback.state.PlaybackStateHolder import org.oxycblt.auxio.playback.state.PlaybackStateHolder
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.Queue
import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.state.StateEvent
import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
@ -219,9 +217,6 @@ class PlaybackService :
// --- PLAYBACKSTATEHOLDER OVERRIDES --- // --- PLAYBACKSTATEHOLDER OVERRIDES ---
override val currentSong
get() = player.song
override val repeatMode override val repeatMode
get() = player.repeat get() = player.repeat
@ -237,16 +232,17 @@ class PlaybackService :
} }
?: Progression.nil() ?: Progression.nil()
override val audioSessionId: Int
get() = player.audioSessionId
override var parent: MusicParent? = null override var parent: MusicParent? = null
override val isShuffled override val isShuffled
get() = player.shuffleModeEnabled get() = player.shuffleModeEnabled
override fun resolveQueue(): Queue = override fun resolveIndex() = player.resolveIndex()
player.song?.let { Queue(player.currentIndex, player.resolveQueue()) } ?: Queue.nil()
override fun resolveQueue() = player.resolveQueue()
override val audioSessionId: Int
get() = player.audioSessionId
override fun newPlayback( override fun newPlayback(
queue: List<Song>, queue: List<Song>,
@ -263,19 +259,12 @@ class PlaybackService :
} }
player.prepare() player.prepare()
player.playWhenReady = play player.playWhenReady = play
playbackManager.dispatchEvent( playbackManager.dispatchEvent(this, StateEvent.NewPlayback)
this,
PlaybackEvent.NewPlayback(
requireNotNull(player.song) {
"Inconsistency detected: Player does not have song despite being populated"
},
Queue(player.currentIndex, player.resolveQueue()),
parent,
isShuffled))
} }
override fun playing(playing: Boolean) { override fun playing(playing: Boolean) {
player.playWhenReady = playing player.playWhenReady = playing
// Dispatched later once all of the changes have been accumulated
} }
override fun repeatMode(repeatMode: RepeatMode) { override fun repeatMode(repeatMode: RepeatMode) {
@ -285,6 +274,7 @@ class PlaybackService :
RepeatMode.ALL -> Player.REPEAT_MODE_ALL RepeatMode.ALL -> Player.REPEAT_MODE_ALL
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
} }
playbackManager.dispatchEvent(this, StateEvent.RepeatModeChanged)
} }
override fun seekTo(positionMs: Long) { override fun seekTo(positionMs: Long) {
@ -293,100 +283,53 @@ class PlaybackService :
override fun next() { override fun next() {
player.seekToNext() player.seekToNext()
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
player.play() player.play()
} }
override fun prev() { override fun prev() {
player.seekToPrevious() player.seekToPrevious()
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
player.play() player.play()
} }
override fun goto(index: Int) { override fun goto(index: Int) {
player.goto(index) player.goto(index)
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
player.play() player.play()
} }
override fun reorder(shuffled: Boolean) { override fun reorder(shuffled: Boolean) {
player.reorder(shuffled) player.reorder(shuffled)
playbackManager.dispatchEvent( playbackManager.dispatchEvent(this, StateEvent.QueueReordered)
this,
PlaybackEvent.QueueReordered(
Queue(
player.currentIndex,
player.resolveQueue(),
),
shuffled))
} }
override fun addToQueue(songs: List<Song>) { override fun addToQueue(songs: List<Song>) {
val insertAt = player.currentIndex + 1 val insertAt = playbackManager.index + 1
player.addToQueue(songs) player.addToQueue(songs)
playbackManager.dispatchEvent( playbackManager.dispatchEvent(
this, this, StateEvent.QueueChanged(UpdateInstructions.Add(insertAt, songs.size), false))
PlaybackEvent.QueueChanged(
requireNotNull(player.song) {
"Inconsistency detected: Player does not have song despite being populated"
},
Queue(player.currentIndex, player.resolveQueue()),
QueueChange(
QueueChange.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size))))
} }
override fun playNext(songs: List<Song>) { override fun playNext(songs: List<Song>) {
val insertAt = player.currentIndex + 1 val insertAt = playbackManager.index + 1
player.playNext(songs) player.playNext(songs)
// TODO: Re-add queue changes
playbackManager.dispatchEvent( playbackManager.dispatchEvent(
this, this, StateEvent.QueueChanged(UpdateInstructions.Add(insertAt, songs.size), false))
PlaybackEvent.QueueChanged(
requireNotNull(player.song) {
"Inconsistency detected: Player does not have song despite being populated"
},
Queue(player.currentIndex, player.resolveQueue()),
QueueChange(
QueueChange.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size))))
} }
override fun move(from: Int, to: Int) { override fun move(from: Int, to: Int) {
val oldIndex = player.currentIndex
player.move(from, to) player.move(from, to)
val changeType =
if (player.currentIndex != oldIndex) {
QueueChange.Type.INDEX
} else {
QueueChange.Type.MAPPING
}
playbackManager.dispatchEvent( playbackManager.dispatchEvent(
this, this, StateEvent.QueueChanged(UpdateInstructions.Move(from, to), false))
PlaybackEvent.QueueChanged(
requireNotNull(player.song) {
"Inconsistency detected: Player does not have song despite being populated"
},
Queue(player.currentIndex, player.resolveQueue()),
QueueChange(changeType, UpdateInstructions.Move(from, to))))
} }
override fun remove(at: Int) { override fun remove(at: Int) {
val oldUnscrambledIndex = player.currentIndex val oldIndex = player.currentMediaItemIndex
val oldScrambledIndex = player.currentMediaItemIndex
player.remove(at) player.remove(at)
val newUnscrambledIndex = player.currentIndex val newIndex = player.currentMediaItemIndex
val newScrambledIndex = player.currentMediaItemIndex
val changeType =
when {
oldScrambledIndex != newScrambledIndex -> QueueChange.Type.SONG
oldUnscrambledIndex != newUnscrambledIndex -> QueueChange.Type.INDEX
else -> QueueChange.Type.MAPPING
}
playbackManager.dispatchEvent( playbackManager.dispatchEvent(
this, this, StateEvent.QueueChanged(UpdateInstructions.Remove(at, 1), oldIndex != newIndex))
PlaybackEvent.QueueChanged(
requireNotNull(player.song) {
"Inconsistency detected: Player does not have song despite being populated"
},
Queue(player.currentIndex, player.resolveQueue()),
QueueChange(changeType, UpdateInstructions.Remove(at, 1))))
} }
// --- PLAYER OVERRIDES --- // --- PLAYER OVERRIDES ---
@ -418,8 +361,7 @@ class PlaybackService :
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO || if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) { reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) {
playbackManager.dispatchEvent( playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
this, PlaybackEvent.IndexMoved(player.song, player.currentIndex))
} }
} }
@ -431,7 +373,7 @@ class PlaybackService :
Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_IS_PLAYING_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY)) { Player.EVENT_POSITION_DISCONTINUITY)) {
logD("Player state changed, must synchronize state") logD("Player state changed, must synchronize state")
playbackManager.dispatchEvent(this, PlaybackEvent.ProgressionChanged(progression)) playbackManager.dispatchEvent(this, StateEvent.ProgressionChanged)
} }
} }
@ -572,7 +514,7 @@ class PlaybackService :
} }
ACTION_INVERT_SHUFFLE -> { ACTION_INVERT_SHUFFLE -> {
logD("Received shuffle event") logD("Received shuffle event")
playbackManager.reorder(!playbackManager.isShuffled) playbackManager.shuffled(!playbackManager.isShuffled)
} }
ACTION_SKIP_PREV -> { ACTION_SKIP_PREV -> {
logD("Received skip previous event") logD("Received skip previous event")

View file

@ -29,10 +29,10 @@ import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.image.ImageSettings
import org.oxycblt.auxio.image.extractor.RoundedRectTransformation import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
import org.oxycblt.auxio.image.extractor.SquareCropTransformation import org.oxycblt.auxio.image.extractor.SquareCropTransformation
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.PlaybackEvent
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.UISettings
@ -135,17 +135,28 @@ constructor(
// --- CALLBACKS --- // --- CALLBACKS ---
override fun onPlaybackEvent(event: PlaybackEvent) { // Respond to all major song or player changes that will affect the widget
if (event is PlaybackEvent.NewPlayback || override fun onIndexMoved(index: Int) = update()
event is PlaybackEvent.ProgressionChanged ||
(event is PlaybackEvent.QueueChanged && event.change.type == QueueChange.Type.SONG) || override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
event is PlaybackEvent.QueueReordered || if (change.type == QueueChange.Type.SONG) {
event is PlaybackEvent.IndexMoved ||
event is PlaybackEvent.RepeatModeChanged) {
update() update()
} }
} }
override fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) = update()
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) = update()
override fun onProgressionChanged(progression: Progression) = update()
override fun onRepeatModeChanged(repeatMode: RepeatMode) = update()
// Respond to settings changes that will affect the widget // Respond to settings changes that will affect the widget
override fun onRoundModeChanged() = update() override fun onRoundModeChanged() = update()
@ -156,7 +167,7 @@ constructor(
* *
* @param song [Queue.currentSong] * @param song [Queue.currentSong]
* @param cover A pre-loaded album cover [Bitmap] for [song]. * @param cover A pre-loaded album cover [Bitmap] for [song].
* @param isPlaying [PlaybackStateManager.progression] * @param isPlaying [PlaybackStateManager.playerState]
* @param repeatMode [PlaybackStateManager.repeatMode] * @param repeatMode [PlaybackStateManager.repeatMode]
* @param isShuffled [Queue.isShuffled] * @param isShuffled [Queue.isShuffled]
*/ */