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:
parent
d5622895d0
commit
1d63ad5b7b
11 changed files with 436 additions and 802 deletions
|
@ -12,6 +12,7 @@
|
|||
#### What's Fixed
|
||||
- Fixed a crash occuring if you navigated to the settings page from the playlist view
|
||||
and then back
|
||||
- Fixed music loading failing with an SQL error with certain music folder configurations
|
||||
|
||||
## 3.3.0
|
||||
|
||||
|
|
|
@ -37,8 +37,8 @@ import org.oxycblt.auxio.music.Playlist
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||
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.Progression
|
||||
import org.oxycblt.auxio.playback.state.QueueChange
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.util.Event
|
||||
|
@ -130,52 +130,56 @@ constructor(
|
|||
playbackSettings.unregisterListener(this)
|
||||
}
|
||||
|
||||
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||
when (event) {
|
||||
is PlaybackEvent.IndexMoved -> {
|
||||
logD("Index moved, updating current song")
|
||||
_song.value = event.currentSong
|
||||
}
|
||||
is PlaybackEvent.QueueChanged -> {
|
||||
// Other types of queue changes preserve the current song.
|
||||
if (event.change.type == QueueChange.Type.SONG) {
|
||||
logD("Queue changed, updating current song")
|
||||
_song.value = event.currentSong
|
||||
override fun onIndexMoved(index: Int) {
|
||||
logD("Index moved, updating current song")
|
||||
_song.value = playbackManager.currentSong
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
|
||||
// Other types of queue changes preserve the current song.
|
||||
if (change.type == QueueChange.Type.SONG) {
|
||||
logD("Queue changed, updating current song")
|
||||
_song.value = playbackManager.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
|
||||
}
|
||||
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 onRepeatModeChanged(repeatMode: RepeatMode) {
|
||||
_repeatMode.value = repeatMode
|
||||
}
|
||||
|
||||
override fun onBarActionChanged() {
|
||||
|
@ -579,7 +583,7 @@ constructor(
|
|||
/** Toggle [isShuffled] (ex. from on to off) */
|
||||
fun toggleShuffled() {
|
||||
logD("Toggling shuffled state")
|
||||
playbackManager.reorder(!playbackManager.isShuffled)
|
||||
playbackManager.shuffled(!playbackManager.isShuffled)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,8 +24,8 @@ import javax.inject.Inject
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
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.QueueChange
|
||||
import org.oxycblt.auxio.util.Event
|
||||
|
@ -52,7 +52,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
|
|||
val scrollTo: Event<Int>
|
||||
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. */
|
||||
val index: StateFlow<Int>
|
||||
get() = _index
|
||||
|
@ -61,45 +61,47 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
|
|||
playbackManager.addListener(this)
|
||||
}
|
||||
|
||||
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||
when (event) {
|
||||
is PlaybackEvent.IndexMoved -> {
|
||||
logD("Index moved, synchronizing and scrolling to new position")
|
||||
_scrollTo.put(event.index)
|
||||
_index.value = event.index
|
||||
}
|
||||
is PlaybackEvent.QueueChanged -> {
|
||||
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
|
||||
logD("Updating queue display")
|
||||
_queueInstructions.put(event.change.instructions)
|
||||
_queue.value = event.queue.queue
|
||||
if (event.change.type != QueueChange.Type.MAPPING) {
|
||||
// Index changed, make sure it remains updated without actually scrolling to it.
|
||||
logD("Index changed with queue, synchronizing new position")
|
||||
_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 onIndexMoved(index: Int) {
|
||||
logD("Index moved, synchronizing and scrolling to new position")
|
||||
_scrollTo.put(index)
|
||||
_index.value = index
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
|
||||
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
|
||||
logD("Updating queue display")
|
||||
_queueInstructions.put(change.instructions)
|
||||
_queue.value = queue
|
||||
if (change.type != QueueChange.Type.MAPPING) {
|
||||
// Index changed, make sure it remains updated without actually scrolling to it.
|
||||
logD("Index changed with queue, synchronizing new position")
|
||||
_index.value = index
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
super.onCleared()
|
||||
playbackManager.removeListener(this)
|
||||
|
|
|
@ -27,9 +27,9 @@ import java.nio.ByteBuffer
|
|||
import javax.inject.Inject
|
||||
import kotlin.math.pow
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
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.QueueChange
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -70,32 +70,28 @@ constructor(
|
|||
|
||||
// --- OVERRIDES ---
|
||||
|
||||
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||
when (event) {
|
||||
is PlaybackEvent.IndexMoved -> {
|
||||
logD("Index moved, updating current song")
|
||||
applyReplayGain(event.currentSong)
|
||||
}
|
||||
is PlaybackEvent.QueueChanged -> {
|
||||
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
|
||||
logD("Updating queue display")
|
||||
// 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 onIndexMoved(index: Int) {
|
||||
logD("Index moved, updating current song")
|
||||
applyReplayGain(playbackManager.currentSong)
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
|
||||
// Other types of queue changes preserve the current song.
|
||||
if (change.type == QueueChange.Type.SONG) {
|
||||
applyReplayGain(playbackManager.currentSong)
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// ReplayGain config changed, we need to set it up again.
|
||||
applyReplayGain(playbackManager.currentSong)
|
||||
|
|
|
@ -27,19 +27,19 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
interface PlaybackStateHolder {
|
||||
val currentSong: Song?
|
||||
val progression: Progression
|
||||
|
||||
val repeatMode: RepeatMode
|
||||
|
||||
val progression: Progression
|
||||
|
||||
val audioSessionId: Int
|
||||
|
||||
val parent: MusicParent?
|
||||
|
||||
fun resolveQueue(): List<Song>
|
||||
|
||||
fun resolveIndex(): Int
|
||||
|
||||
val isShuffled: Boolean
|
||||
|
||||
fun resolveQueue(): Queue
|
||||
val audioSessionId: Int
|
||||
|
||||
fun newPlayback(
|
||||
queue: List<Song>,
|
||||
|
@ -74,6 +74,21 @@ interface PlaybackStateHolder {
|
|||
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.
|
||||
*
|
||||
|
@ -114,9 +129,9 @@ sealed interface 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 {
|
||||
fun nil() = Queue(-1, emptyList())
|
||||
fun nil() = Queue(emptyList(), -1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,17 +46,17 @@ interface PlaybackStateManager {
|
|||
/** The current [Progression] state. */
|
||||
val progression: Progression
|
||||
|
||||
val currentSong: Song?
|
||||
|
||||
val repeatMode: RepeatMode
|
||||
|
||||
val audioSessionId: Int
|
||||
|
||||
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. */
|
||||
val currentAudioSessionId: Int?
|
||||
|
@ -178,9 +178,9 @@ interface PlaybackStateManager {
|
|||
*
|
||||
* @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.
|
||||
|
@ -240,7 +240,54 @@ interface PlaybackStateManager {
|
|||
* [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener].
|
||||
*/
|
||||
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 {
|
||||
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>()
|
||||
|
||||
@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 pendingDeferredPlayback: DeferredPlayback? = null
|
||||
@Volatile private var isInitialized = false
|
||||
|
||||
/** The current [Progression] state. */
|
||||
override val progression: Progression
|
||||
get() = stateHolder?.progression ?: Progression.nil()
|
||||
override val progression
|
||||
get() = stateMirror.progression
|
||||
|
||||
override val currentSong: Song?
|
||||
get() = stateHolder?.currentSong
|
||||
override val repeatMode
|
||||
get() = stateMirror.repeatMode
|
||||
|
||||
override val repeatMode: RepeatMode
|
||||
get() = stateHolder?.repeatMode ?: RepeatMode.NONE
|
||||
override val parent
|
||||
get() = stateMirror.parent
|
||||
|
||||
override val audioSessionId: Int
|
||||
get() = stateHolder?.audioSessionId ?: -1
|
||||
override val currentSong
|
||||
get() = stateMirror.queue.getOrNull(stateMirror.index)
|
||||
|
||||
override val parent: MusicParent?
|
||||
get() = stateHolder?.parent
|
||||
override val queue
|
||||
get() = stateMirror.queue
|
||||
|
||||
override val isShuffled: Boolean
|
||||
get() = stateHolder?.isShuffled ?: false
|
||||
override val index
|
||||
get() = stateMirror.index
|
||||
|
||||
override fun resolveQueue() = stateHolder?.resolveQueue() ?: Queue.nil()
|
||||
override val isShuffled
|
||||
get() = stateMirror.isShuffled
|
||||
|
||||
override val currentAudioSessionId: Int?
|
||||
get() = stateHolder?.audioSessionId
|
||||
|
@ -313,6 +360,14 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
override fun addListener(listener: PlaybackStateManager.Listener) {
|
||||
logD("Adding $listener to listeners")
|
||||
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
|
||||
|
@ -418,7 +473,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
override fun reorder(shuffled: Boolean) {
|
||||
override fun shuffled(shuffled: Boolean) {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Reordering queue [shuffled=$shuffled]")
|
||||
stateHolder.reorder(shuffled)
|
||||
|
@ -470,15 +525,79 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
override fun dispatchEvent(stateHolder: PlaybackStateHolder, vararg events: PlaybackEvent) {
|
||||
override fun dispatchEvent(stateHolder: PlaybackStateHolder, event: StateEvent) {
|
||||
if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) {
|
||||
logW("Given internal player did not match current internal player")
|
||||
return
|
||||
}
|
||||
|
||||
events.forEach { event ->
|
||||
logD("Dispatching event $event")
|
||||
listeners.forEach { it.onPlaybackEvent(event) }
|
||||
when (event) {
|
||||
is StateEvent.IndexMoved -> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,19 @@ import org.oxycblt.auxio.util.logD
|
|||
val ExoPlayer.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?) {
|
||||
clearMediaItems()
|
||||
shuffleModeEnabled = false
|
||||
|
@ -60,25 +73,6 @@ fun ExoPlayer.shuffledQueue(queue: Collection<Song>, start: Song?) {
|
|||
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) {
|
||||
logD("Reordering queue to $shuffled")
|
||||
shuffleModeEnabled = shuffled
|
||||
|
@ -97,23 +91,23 @@ fun ExoPlayer.addToQueue(songs: List<Song>) {
|
|||
}
|
||||
|
||||
fun ExoPlayer.goto(index: Int) {
|
||||
val queue = unscrambleQueue { index -> index }
|
||||
if (queue.isEmpty()) {
|
||||
val indices = unscrambleQueueIndices()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = queue[index]
|
||||
val trueIndex = indices[index]
|
||||
seekTo(trueIndex, C.TIME_UNSET)
|
||||
}
|
||||
|
||||
fun ExoPlayer.move(from: Int, to: Int) {
|
||||
val queue = unscrambleQueue { index -> index }
|
||||
if (queue.isEmpty()) {
|
||||
val indices = unscrambleQueueIndices()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueFrom = queue[from]
|
||||
val trueTo = queue[to]
|
||||
val trueFrom = indices[from]
|
||||
val trueTo = indices[to]
|
||||
|
||||
when {
|
||||
trueFrom > trueTo -> {
|
||||
|
@ -128,29 +122,25 @@ fun ExoPlayer.move(from: Int, to: Int) {
|
|||
}
|
||||
|
||||
fun ExoPlayer.remove(at: Int) {
|
||||
val queue = unscrambleQueue { index -> index }
|
||||
if (queue.isEmpty()) {
|
||||
val indices = unscrambleQueueIndices()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = queue[at]
|
||||
val trueIndex = indices[at]
|
||||
removeMediaItem(trueIndex)
|
||||
}
|
||||
|
||||
fun ExoPlayer.resolveQueue(): List<Song> {
|
||||
return unscrambleQueue { index -> getMediaItemAt(index).song }
|
||||
}
|
||||
|
||||
inline fun <T> ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List<T> {
|
||||
fun ExoPlayer.unscrambleQueueIndices(): List<Int> {
|
||||
val timeline = currentTimeline
|
||||
if (timeline.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val queue = mutableListOf<T>()
|
||||
val queue = mutableListOf<Int>()
|
||||
|
||||
// Add the active queue item.
|
||||
val currentMediaItemIndex = currentMediaItemIndex
|
||||
queue.add(mapper(currentMediaItemIndex))
|
||||
queue.add(currentMediaItemIndex)
|
||||
|
||||
// Fill queue alternating with next and/or previous queue items.
|
||||
var firstMediaItemIndex = currentMediaItemIndex
|
||||
|
@ -164,7 +154,7 @@ inline fun <T> ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List<T> {
|
|||
timeline.getNextWindowIndex(
|
||||
lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(mapper(lastMediaItemIndex))
|
||||
queue.add(lastMediaItemIndex)
|
||||
}
|
||||
}
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
|
@ -172,7 +162,7 @@ inline fun <T> ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List<T> {
|
|||
timeline.getPreviousWindowIndex(
|
||||
firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(0, mapper(firstMediaItemIndex))
|
||||
queue.add(0, firstMediaItemIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,9 +38,8 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.playback.ActionMode
|
||||
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.Queue
|
||||
import org.oxycblt.auxio.playback.state.Progression
|
||||
import org.oxycblt.auxio.playback.state.QueueChange
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -118,60 +117,66 @@ constructor(
|
|||
|
||||
// --- PLAYBACKSTATEMANAGER OVERRIDES ---
|
||||
|
||||
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||
when (event) {
|
||||
is PlaybackEvent.IndexMoved -> {
|
||||
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
|
||||
})
|
||||
override fun onIndexMoved(index: Int) {
|
||||
updateMediaMetadata(playbackManager.currentSong, playbackManager.parent)
|
||||
invalidateSessionState()
|
||||
}
|
||||
|
||||
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 ---
|
||||
|
||||
override fun onImageSettingsChanged() {
|
||||
|
@ -251,7 +256,7 @@ constructor(
|
|||
}
|
||||
|
||||
override fun onSetShuffleMode(shuffleMode: Int) {
|
||||
playbackManager.reorder(
|
||||
playbackManager.shuffled(
|
||||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
|
||||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP)
|
||||
}
|
||||
|
@ -355,9 +360,9 @@ constructor(
|
|||
*
|
||||
* @param queue The current queue to upload.
|
||||
*/
|
||||
private fun updateQueue(queue: Queue) {
|
||||
private fun updateQueue(queue: List<Song>) {
|
||||
val queueItems =
|
||||
queue.queue.mapIndexed { i, song ->
|
||||
queue.mapIndexed { i, song ->
|
||||
val description =
|
||||
MediaDescriptionCompat.Builder()
|
||||
// Media ID should not be the item index but rather the UID,
|
||||
|
@ -382,15 +387,13 @@ constructor(
|
|||
private fun invalidateSessionState() {
|
||||
logD("Updating media session playback state")
|
||||
|
||||
val queue = playbackManager.resolveQueue()
|
||||
|
||||
val state =
|
||||
// InternalPlayer.State handles position/state information.
|
||||
playbackManager.progression
|
||||
.intoPlaybackState(PlaybackStateCompat.Builder())
|
||||
.setActions(ACTIONS)
|
||||
// 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.
|
||||
|
||||
|
|
|
@ -56,13 +56,11 @@ import org.oxycblt.auxio.playback.PlaybackSettings
|
|||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
||||
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.PlaybackStateManager
|
||||
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.StateEvent
|
||||
import org.oxycblt.auxio.service.ForegroundManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
@ -219,9 +217,6 @@ class PlaybackService :
|
|||
|
||||
// --- PLAYBACKSTATEHOLDER OVERRIDES ---
|
||||
|
||||
override val currentSong
|
||||
get() = player.song
|
||||
|
||||
override val repeatMode
|
||||
get() = player.repeat
|
||||
|
||||
|
@ -237,16 +232,17 @@ class PlaybackService :
|
|||
}
|
||||
?: Progression.nil()
|
||||
|
||||
override val audioSessionId: Int
|
||||
get() = player.audioSessionId
|
||||
|
||||
override var parent: MusicParent? = null
|
||||
|
||||
override val isShuffled
|
||||
get() = player.shuffleModeEnabled
|
||||
|
||||
override fun resolveQueue(): Queue =
|
||||
player.song?.let { Queue(player.currentIndex, player.resolveQueue()) } ?: Queue.nil()
|
||||
override fun resolveIndex() = player.resolveIndex()
|
||||
|
||||
override fun resolveQueue() = player.resolveQueue()
|
||||
|
||||
override val audioSessionId: Int
|
||||
get() = player.audioSessionId
|
||||
|
||||
override fun newPlayback(
|
||||
queue: List<Song>,
|
||||
|
@ -263,19 +259,12 @@ class PlaybackService :
|
|||
}
|
||||
player.prepare()
|
||||
player.playWhenReady = play
|
||||
playbackManager.dispatchEvent(
|
||||
this,
|
||||
PlaybackEvent.NewPlayback(
|
||||
requireNotNull(player.song) {
|
||||
"Inconsistency detected: Player does not have song despite being populated"
|
||||
},
|
||||
Queue(player.currentIndex, player.resolveQueue()),
|
||||
parent,
|
||||
isShuffled))
|
||||
playbackManager.dispatchEvent(this, StateEvent.NewPlayback)
|
||||
}
|
||||
|
||||
override fun playing(playing: Boolean) {
|
||||
player.playWhenReady = playing
|
||||
// Dispatched later once all of the changes have been accumulated
|
||||
}
|
||||
|
||||
override fun repeatMode(repeatMode: RepeatMode) {
|
||||
|
@ -285,6 +274,7 @@ class PlaybackService :
|
|||
RepeatMode.ALL -> Player.REPEAT_MODE_ALL
|
||||
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
||||
}
|
||||
playbackManager.dispatchEvent(this, StateEvent.RepeatModeChanged)
|
||||
}
|
||||
|
||||
override fun seekTo(positionMs: Long) {
|
||||
|
@ -293,100 +283,53 @@ class PlaybackService :
|
|||
|
||||
override fun next() {
|
||||
player.seekToNext()
|
||||
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
|
||||
player.play()
|
||||
}
|
||||
|
||||
override fun prev() {
|
||||
player.seekToPrevious()
|
||||
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
|
||||
player.play()
|
||||
}
|
||||
|
||||
override fun goto(index: Int) {
|
||||
player.goto(index)
|
||||
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
|
||||
player.play()
|
||||
}
|
||||
|
||||
override fun reorder(shuffled: Boolean) {
|
||||
player.reorder(shuffled)
|
||||
playbackManager.dispatchEvent(
|
||||
this,
|
||||
PlaybackEvent.QueueReordered(
|
||||
Queue(
|
||||
player.currentIndex,
|
||||
player.resolveQueue(),
|
||||
),
|
||||
shuffled))
|
||||
playbackManager.dispatchEvent(this, StateEvent.QueueReordered)
|
||||
}
|
||||
|
||||
override fun addToQueue(songs: List<Song>) {
|
||||
val insertAt = player.currentIndex + 1
|
||||
val insertAt = playbackManager.index + 1
|
||||
player.addToQueue(songs)
|
||||
playbackManager.dispatchEvent(
|
||||
this,
|
||||
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))))
|
||||
this, StateEvent.QueueChanged(UpdateInstructions.Add(insertAt, songs.size), false))
|
||||
}
|
||||
|
||||
override fun playNext(songs: List<Song>) {
|
||||
val insertAt = player.currentIndex + 1
|
||||
val insertAt = playbackManager.index + 1
|
||||
player.playNext(songs)
|
||||
// TODO: Re-add queue changes
|
||||
playbackManager.dispatchEvent(
|
||||
this,
|
||||
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))))
|
||||
this, StateEvent.QueueChanged(UpdateInstructions.Add(insertAt, songs.size), false))
|
||||
}
|
||||
|
||||
override fun move(from: Int, to: Int) {
|
||||
val oldIndex = player.currentIndex
|
||||
player.move(from, to)
|
||||
val changeType =
|
||||
if (player.currentIndex != oldIndex) {
|
||||
QueueChange.Type.INDEX
|
||||
} else {
|
||||
QueueChange.Type.MAPPING
|
||||
}
|
||||
playbackManager.dispatchEvent(
|
||||
this,
|
||||
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))))
|
||||
this, StateEvent.QueueChanged(UpdateInstructions.Move(from, to), false))
|
||||
}
|
||||
|
||||
override fun remove(at: Int) {
|
||||
val oldUnscrambledIndex = player.currentIndex
|
||||
val oldScrambledIndex = player.currentMediaItemIndex
|
||||
val oldIndex = player.currentMediaItemIndex
|
||||
player.remove(at)
|
||||
val newUnscrambledIndex = player.currentIndex
|
||||
val newScrambledIndex = player.currentMediaItemIndex
|
||||
val changeType =
|
||||
when {
|
||||
oldScrambledIndex != newScrambledIndex -> QueueChange.Type.SONG
|
||||
oldUnscrambledIndex != newUnscrambledIndex -> QueueChange.Type.INDEX
|
||||
else -> QueueChange.Type.MAPPING
|
||||
}
|
||||
|
||||
val newIndex = player.currentMediaItemIndex
|
||||
playbackManager.dispatchEvent(
|
||||
this,
|
||||
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))))
|
||||
this, StateEvent.QueueChanged(UpdateInstructions.Remove(at, 1), oldIndex != newIndex))
|
||||
}
|
||||
|
||||
// --- PLAYER OVERRIDES ---
|
||||
|
@ -418,8 +361,7 @@ class PlaybackService :
|
|||
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
|
||||
reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) {
|
||||
playbackManager.dispatchEvent(
|
||||
this, PlaybackEvent.IndexMoved(player.song, player.currentIndex))
|
||||
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -431,7 +373,7 @@ class PlaybackService :
|
|||
Player.EVENT_IS_PLAYING_CHANGED,
|
||||
Player.EVENT_POSITION_DISCONTINUITY)) {
|
||||
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 -> {
|
||||
logD("Received shuffle event")
|
||||
playbackManager.reorder(!playbackManager.isShuffled)
|
||||
playbackManager.shuffled(!playbackManager.isShuffled)
|
||||
}
|
||||
ACTION_SKIP_PREV -> {
|
||||
logD("Received skip previous event")
|
||||
|
|
|
@ -29,10 +29,10 @@ import org.oxycblt.auxio.image.BitmapProvider
|
|||
import org.oxycblt.auxio.image.ImageSettings
|
||||
import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
|
||||
import org.oxycblt.auxio.image.extractor.SquareCropTransformation
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
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.Progression
|
||||
import org.oxycblt.auxio.playback.state.QueueChange
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
|
@ -135,17 +135,28 @@ constructor(
|
|||
|
||||
// --- CALLBACKS ---
|
||||
|
||||
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||
if (event is PlaybackEvent.NewPlayback ||
|
||||
event is PlaybackEvent.ProgressionChanged ||
|
||||
(event is PlaybackEvent.QueueChanged && event.change.type == QueueChange.Type.SONG) ||
|
||||
event is PlaybackEvent.QueueReordered ||
|
||||
event is PlaybackEvent.IndexMoved ||
|
||||
event is PlaybackEvent.RepeatModeChanged) {
|
||||
// Respond to all major song or player changes that will affect the widget
|
||||
override fun onIndexMoved(index: Int) = update()
|
||||
|
||||
override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
|
||||
if (change.type == QueueChange.Type.SONG) {
|
||||
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
|
||||
override fun onRoundModeChanged() = update()
|
||||
|
||||
|
@ -156,7 +167,7 @@ constructor(
|
|||
*
|
||||
* @param song [Queue.currentSong]
|
||||
* @param cover A pre-loaded album cover [Bitmap] for [song].
|
||||
* @param isPlaying [PlaybackStateManager.progression]
|
||||
* @param isPlaying [PlaybackStateManager.playerState]
|
||||
* @param repeatMode [PlaybackStateManager.repeatMode]
|
||||
* @param isShuffled [Queue.isShuffled]
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue