playback: implement new queue system

Implement a new heap-based queue system into the app.

Unlike the prior queue, this one no longer keeps one canonical queue
and then simply swaps between shuffled and ordered queues by
re-grabbing the song list currently being played. Instead, the same
"heap" of songs is used throughout, with only the way they are
interpreted changing.

This enables a bunch of near functionality that would not be possible
with the prior queue system, but is also really unstable and needs a
lot more testing.

Currently this commit disables state saving at the moment. It will be
re-enabled when the new queue can be reliably restored.
This commit is contained in:
Alexander Capehart 2023-01-04 19:08:45 -07:00
parent 1b44eeae15
commit 16513e6547
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 501 additions and 349 deletions

View file

@ -30,9 +30,9 @@ import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.storage.*
import org.oxycblt.auxio.music.parsing.parseId3GenreNames import org.oxycblt.auxio.music.parsing.parseId3GenreNames
import org.oxycblt.auxio.music.parsing.parseMultiValue import org.oxycblt.auxio.music.parsing.parseMultiValue
import org.oxycblt.auxio.music.storage.*
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull

View file

@ -29,6 +29,7 @@ import androidx.core.database.getStringOrNull
import java.io.File import java.io.File
import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.Date
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.parsing.parseId3v2Position
import org.oxycblt.auxio.music.storage.Directory import org.oxycblt.auxio.music.storage.Directory
import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.music.storage.directoryCompat import org.oxycblt.auxio.music.storage.directoryCompat
@ -36,7 +37,6 @@ import org.oxycblt.auxio.music.storage.mediaStoreVolumeNameCompat
import org.oxycblt.auxio.music.storage.safeQuery import org.oxycblt.auxio.music.storage.safeQuery
import org.oxycblt.auxio.music.storage.storageVolumesCompat import org.oxycblt.auxio.music.storage.storageVolumesCompat
import org.oxycblt.auxio.music.storage.useQuery import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.music.parsing.parseId3v2Position
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

@ -23,8 +23,8 @@ import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MetadataRetriever import com.google.android.exoplayer2.MetadataRetriever
import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.Date
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.storage.toAudioUri
import org.oxycblt.auxio.music.parsing.parseId3v2Position import org.oxycblt.auxio.music.parsing.parseId3v2Position
import org.oxycblt.auxio.music.storage.toAudioUri
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW

View file

@ -26,10 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.*
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
@ -100,13 +97,18 @@ class PlaybackViewModel(application: Application) :
playbackManager.removeListener(this) playbackManager.removeListener(this)
} }
override fun onIndexMoved(index: Int) { override fun onIndexMoved(queue: Queue) {
_song.value = playbackManager.song _song.value = playbackManager.queue.currentSong
} }
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) { override fun onQueueReworked(queue: Queue) {
_song.value = playbackManager.song _isShuffled.value = queue.isShuffled
}
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
_song.value = playbackManager.queue.currentSong
_parent.value = playbackManager.parent _parent.value = playbackManager.parent
_isShuffled.value = queue.isShuffled
} }
override fun onStateChanged(state: InternalPlayer.State) { override fun onStateChanged(state: InternalPlayer.State) {
@ -126,10 +128,6 @@ class PlaybackViewModel(application: Application) :
} }
} }
override fun onShuffledChanged(isShuffled: Boolean) {
_isShuffled.value = isShuffled
}
override fun onRepeatChanged(repeatMode: RepeatMode) { override fun onRepeatChanged(repeatMode: RepeatMode) {
_repeatMode.value = repeatMode _repeatMode.value = repeatMode
} }
@ -141,21 +139,19 @@ class PlaybackViewModel(application: Application) :
* @param song The [Song] to play. * @param song The [Song] to play.
*/ */
fun playFromAll(song: Song) { fun playFromAll(song: Song) {
playbackManager.play(song, null, settings) playImpl(song, null)
} }
/** Shuffle all songs in the music library. */ /** Shuffle all songs in the music library. */
fun shuffleAll() { fun shuffleAll() {
playbackManager.play(null, null, settings, true) playImpl(null, null, true)
} }
/** /**
* Play a [Song] from it's [Album]. * Play a [Song] from it's [Album].
* @param song The [Song] to play. * @param song The [Song] to play.
*/ */
fun playFromAlbum(song: Song) { fun playFromAlbum(song: Song) = playImpl(song, song.album)
playbackManager.play(song, song.album, settings)
}
/** /**
* Play a [Song] from one of it's [Artist]s. * Play a [Song] from one of it's [Artist]s.
@ -165,10 +161,9 @@ class PlaybackViewModel(application: Application) :
*/ */
fun playFromArtist(song: Song, artist: Artist? = null) { fun playFromArtist(song: Song, artist: Artist? = null) {
if (artist != null) { if (artist != null) {
check(artist in song.artists) { "Artist not in song artists" } playImpl(song, artist)
playbackManager.play(song, artist, settings)
} else if (song.artists.size == 1) { } else if (song.artists.size == 1) {
playbackManager.play(song, song.artists[0], settings) playImpl(song, song.artists[0])
} else { } else {
_artistPlaybackPickerSong.value = song _artistPlaybackPickerSong.value = song
} }
@ -191,10 +186,9 @@ class PlaybackViewModel(application: Application) :
*/ */
fun playFromGenre(song: Song, genre: Genre? = null) { fun playFromGenre(song: Song, genre: Genre? = null) {
if (genre != null) { if (genre != null) {
check(genre.songs.contains(song)) { "Invalid input: Genre is not linked to song" } playImpl(song, genre)
playbackManager.play(song, genre, settings)
} else if (song.genres.size == 1) { } else if (song.genres.size == 1) {
playbackManager.play(song, song.genres[0], settings) playImpl(song, song.genres[0])
} else { } else {
_genrePlaybackPickerSong.value = song _genrePlaybackPickerSong.value = song
} }
@ -204,49 +198,37 @@ class PlaybackViewModel(application: Application) :
* Play an [Album]. * Play an [Album].
* @param album The [Album] to play. * @param album The [Album] to play.
*/ */
fun play(album: Album) { fun play(album: Album) = playImpl(null, album, false)
playbackManager.play(null, album, settings, false)
}
/** /**
* Play an [Artist]. * Play an [Artist].
* @param artist The [Artist] to play. * @param artist The [Artist] to play.
*/ */
fun play(artist: Artist) { fun play(artist: Artist) = playImpl(null, artist, false)
playbackManager.play(null, artist, settings, false)
}
/** /**
* Play a [Genre]. * Play a [Genre].
* @param genre The [Genre] to play. * @param genre The [Genre] to play.
*/ */
fun play(genre: Genre) { fun play(genre: Genre) = playImpl(null, genre, false)
playbackManager.play(null, genre, settings, false)
}
/** /**
* Shuffle an [Album]. * Shuffle an [Album].
* @param album The [Album] to shuffle. * @param album The [Album] to shuffle.
*/ */
fun shuffle(album: Album) { fun shuffle(album: Album) = playImpl(null, album, true)
playbackManager.play(null, album, settings, true)
}
/** /**
* Shuffle an [Artist]. * Shuffle an [Artist].
* @param artist The [Artist] to shuffle. * @param artist The [Artist] to shuffle.
*/ */
fun shuffle(artist: Artist) { fun shuffle(artist: Artist) = playImpl(null, artist, true)
playbackManager.play(null, artist, settings, true)
}
/** /**
* Shuffle an [Genre]. * Shuffle an [Genre].
* @param genre The [Genre] to shuffle. * @param genre The [Genre] to shuffle.
*/ */
fun shuffle(genre: Genre) { fun shuffle(genre: Genre) = playImpl(null, genre, true)
playbackManager.play(null, genre, settings, true)
}
/** /**
* Start the given [InternalPlayer.Action] to be completed eventually. This can be used to * Start the given [InternalPlayer.Action] to be completed eventually. This can be used to
@ -257,6 +239,24 @@ class PlaybackViewModel(application: Application) :
playbackManager.startAction(action) playbackManager.startAction(action)
} }
private fun playImpl(
song: Song?,
parent: MusicParent?,
shuffled: Boolean = playbackManager.queue.isShuffled && settings.keepShuffle
) {
check(song == null || parent == null || parent.songs.contains(song)) {
"Song to play not in parent"
}
val sort =
when (parent) {
is Genre -> settings.detailGenreSort
is Artist -> settings.detailArtistSort
is Album -> settings.detailAlbumSort
null -> settings.libSongSort
}
playbackManager.play(song, parent, sort, shuffled)
}
// --- PLAYER FUNCTIONS --- // --- PLAYER FUNCTIONS ---
/** /**
@ -370,7 +370,7 @@ class PlaybackViewModel(application: Application) :
/** Toggle [isShuffled] (ex. from on to off) */ /** Toggle [isShuffled] (ex. from on to off) */
fun invertShuffled() { fun invertShuffled() {
playbackManager.reshuffle(!playbackManager.isShuffled, settings) playbackManager.reorder(!playbackManager.queue.isShuffled)
} }
/** /**

View file

@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Queue
/** /**
* A [ViewModel] that manages the current queue state and allows navigation through the queue. * A [ViewModel] that manages the current queue state and allows navigation through the queue.
@ -36,7 +37,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
/** The current queue. */ /** The current queue. */
val queue: StateFlow<List<Song>> = _queue val queue: StateFlow<List<Song>> = _queue
private val _index = MutableStateFlow(playbackManager.index) private val _index = MutableStateFlow(playbackManager.queue.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
@ -56,10 +57,6 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
* range. * range.
*/ */
fun goto(adapterIndex: Int) { fun goto(adapterIndex: Int) {
if (adapterIndex !in playbackManager.queue.indices) {
// Invalid input. Nothing to do.
return
}
playbackManager.goto(adapterIndex) playbackManager.goto(adapterIndex)
} }
@ -69,10 +66,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
* range. * range.
*/ */
fun removeQueueDataItem(adapterIndex: Int) { fun removeQueueDataItem(adapterIndex: Int) {
if (adapterIndex <= playbackManager.index || if (adapterIndex !in queue.value.indices) {
adapterIndex !in playbackManager.queue.indices) {
// Invalid input. Nothing to do.
// TODO: Allow editing played queue items.
return return
} }
playbackManager.removeQueueItem(adapterIndex) playbackManager.removeQueueItem(adapterIndex)
@ -85,7 +79,8 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
* @return true if the items were moved, false otherwise. * @return true if the items were moved, false otherwise.
*/ */
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean { fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean {
if (adapterFrom <= playbackManager.index || adapterTo <= playbackManager.index) { if (adapterFrom <= playbackManager.queue.index ||
adapterTo <= playbackManager.queue.index) {
// Invalid input. Nothing to do. // Invalid input. Nothing to do.
return false return false
} }
@ -103,34 +98,34 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
scrollTo = null scrollTo = null
} }
override fun onIndexMoved(index: Int) { override fun onIndexMoved(queue: Queue) {
// Index moved -> Scroll to new index // Index moved -> Scroll to new index
replaceQueue = null replaceQueue = null
scrollTo = index scrollTo = queue.index
_index.value = index _index.value = queue.index
} }
override fun onQueueChanged(queue: List<Song>) { override fun onQueueChanged(queue: Queue) {
// Queue changed trivially due to item move -> Diff queue, stay at current index. // Queue changed trivially due to item move -> Diff queue, stay at current index.
replaceQueue = false replaceQueue = false
scrollTo = null scrollTo = null
_queue.value = playbackManager.queue.toMutableList() _queue.value = queue.resolve()
} }
override fun onQueueReworked(index: Int, queue: List<Song>) { override fun onQueueReworked(queue: Queue) {
// Queue changed completely -> Replace queue, update index // Queue changed completely -> Replace queue, update index
replaceQueue = true replaceQueue = true
scrollTo = index scrollTo = queue.index
_queue.value = playbackManager.queue.toMutableList() _queue.value = queue.resolve()
_index.value = index _index.value = queue.index
} }
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) { override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
// Entirely new queue -> Replace queue, update index // Entirely new queue -> Replace queue, update index
replaceQueue = true replaceQueue = true
scrollTo = index scrollTo = queue.index
_queue.value = playbackManager.queue.toMutableList() _queue.value = queue.resolve()
_index.value = index _index.value = queue.index
} }
override fun onCleared() { override fun onCleared() {

View file

@ -133,7 +133,7 @@ class ReplayGainAudioProcessor(private val context: Context) :
// User wants album gain to be used when in an album, track gain otherwise. // User wants album gain to be used when in an album, track gain otherwise.
ReplayGainMode.DYNAMIC -> ReplayGainMode.DYNAMIC ->
playbackManager.parent is Album && playbackManager.parent is Album &&
playbackManager.song?.album == playbackManager.parent playbackManager.queue.currentSong?.album == playbackManager.parent
} }
val resolvedGain = val resolvedGain =

View file

@ -17,18 +17,11 @@
package org.oxycblt.auxio.playback.state package org.oxycblt.auxio.playback.state
import kotlin.math.max
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
@ -59,22 +52,13 @@ class PlaybackStateManager private constructor() {
@Volatile private var pendingAction: InternalPlayer.Action? = null @Volatile private var pendingAction: InternalPlayer.Action? = null
@Volatile private var isInitialized = false @Volatile private var isInitialized = false
/** The currently playing [Song]. Null if nothing is playing. */ /** The current [Queue]. */
val song val queue = Queue()
get() = queue.getOrNull(index)
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */ /** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
@Volatile @Volatile
var parent: MusicParent? = null var parent: MusicParent? = null
private set private set
@Volatile private var _queue = mutableListOf<Song>()
/** The current queue. */
val queue
get() = _queue
/** The position of the currently playing item in the queue. */
@Volatile
var index = -1
private set
/** The current [InternalPlayer] state. */ /** The current [InternalPlayer] state. */
@Volatile @Volatile
var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0) var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
@ -86,13 +70,8 @@ class PlaybackStateManager private constructor() {
field = value field = value
notifyRepeatModeChanged() notifyRepeatModeChanged()
} }
/** Whether the queue is shuffled. */
@Volatile
var isShuffled = false
private set
/** /**
* The current audio session ID of the internal player. Null if no [InternalPlayer] is * The current audio session ID of the internal player. Null if [InternalPlayer] is unavailable.
* available.
*/ */
val currentAudioSessionId: Int? val currentAudioSessionId: Int?
get() = internalPlayer?.audioSessionId get() = internalPlayer?.audioSessionId
@ -106,9 +85,8 @@ class PlaybackStateManager private constructor() {
@Synchronized @Synchronized
fun addListener(listener: Listener) { fun addListener(listener: Listener) {
if (isInitialized) { if (isInitialized) {
listener.onNewPlayback(index, queue, parent) listener.onNewPlayback(queue, parent)
listener.onRepeatChanged(repeatMode) listener.onRepeatChanged(repeatMode)
listener.onShuffledChanged(isShuffled)
listener.onStateChanged(playerState) listener.onStateChanged(playerState)
} }
@ -141,7 +119,7 @@ class PlaybackStateManager private constructor() {
} }
if (isInitialized) { if (isInitialized) {
internalPlayer.loadSong(song, playerState.isPlaying) internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
internalPlayer.seekTo(playerState.calculateElapsedPositionMs()) internalPlayer.seekTo(playerState.calculateElapsedPositionMs())
// See if there's any action that has been queued. // See if there's any action that has been queued.
requestAction(internalPlayer) requestAction(internalPlayer)
@ -175,27 +153,19 @@ class PlaybackStateManager private constructor() {
* @param song A particular [Song] to play, or null to play the first [Song] in the new queue. * @param song A particular [Song] to play, or null to play the first [Song] in the new queue.
* @param parent The [MusicParent] to play from, or null if to play from the entire * @param parent The [MusicParent] to play from, or null if to play from the entire
* [MusicStore.Library]. * [MusicStore.Library].
* @param settings [Settings] required to configure the queue. * @param sort [Sort] to initially sort an ordered queue with.
* @param shuffled Whether to shuffle the queue. Defaults to the "Remember shuffle" * @param shuffled Whether to shuffle or not.
* configuration.
*/ */
@Synchronized @Synchronized
fun play( fun play(song: Song?, parent: MusicParent?, sort: Sort, shuffled: Boolean) {
song: Song?,
parent: MusicParent?,
settings: Settings,
shuffled: Boolean = settings.keepShuffle && isShuffled
) {
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
val library = musicStore.library ?: return val library = musicStore.library ?: return
// Setup parent and queue // Set up parent and queue
this.parent = parent this.parent = parent
_queue = (parent?.songs ?: library.songs).toMutableList() queue.start(song, sort.songs(parent?.songs ?: library.songs), shuffled)
orderQueue(settings, shuffled, song)
// Notify components of changes // Notify components of changes
notifyNewPlayback() notifyNewPlayback()
notifyShuffledChanged() internalPlayer.loadSong(queue.currentSong, true)
internalPlayer.loadSong(this.song, true)
// Played something, so we are initialized now // Played something, so we are initialized now
isInitialized = true isInitialized = true
} }
@ -209,13 +179,13 @@ class PlaybackStateManager private constructor() {
@Synchronized @Synchronized
fun next() { fun next() {
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
// Increment the index, if it cannot be incremented any further, then var play = true
// repeat and pause/resume playback depending on the setting if (!queue.goto(queue.index + 1)) {
if (index < _queue.lastIndex) { queue.goto(0)
gotoImpl(internalPlayer, index + 1, true) play = false
} else {
gotoImpl(internalPlayer, 0, repeatMode == RepeatMode.ALL)
} }
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, play)
} }
/** /**
@ -231,7 +201,11 @@ class PlaybackStateManager private constructor() {
rewind() rewind()
setPlaying(true) setPlaying(true)
} else { } else {
gotoImpl(internalPlayer, max(index - 1, 0), true) if (!queue.goto(queue.index - 1)) {
queue.goto(0)
}
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true)
} }
} }
@ -242,13 +216,10 @@ class PlaybackStateManager private constructor() {
@Synchronized @Synchronized
fun goto(index: Int) { fun goto(index: Int) {
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
gotoImpl(internalPlayer, index, true) if (queue.goto(index)) {
}
private fun gotoImpl(internalPlayer: InternalPlayer, idx: Int, play: Boolean) {
index = idx
notifyIndexMoved() notifyIndexMoved()
internalPlayer.loadSong(song, play) internalPlayer.loadSong(queue.currentSong, true)
}
} }
/** /**
@ -257,7 +228,7 @@ class PlaybackStateManager private constructor() {
*/ */
@Synchronized @Synchronized
fun playNext(song: Song) { fun playNext(song: Song) {
_queue.add(index + 1, song) queue.playNext(listOf(song))
notifyQueueChanged() notifyQueueChanged()
} }
@ -267,7 +238,7 @@ class PlaybackStateManager private constructor() {
*/ */
@Synchronized @Synchronized
fun playNext(songs: List<Song>) { fun playNext(songs: List<Song>) {
_queue.addAll(index + 1, songs) queue.playNext(songs)
notifyQueueChanged() notifyQueueChanged()
} }
@ -277,7 +248,7 @@ class PlaybackStateManager private constructor() {
*/ */
@Synchronized @Synchronized
fun addToQueue(song: Song) { fun addToQueue(song: Song) {
_queue.add(song) queue.addToQueue(listOf(song))
notifyQueueChanged() notifyQueueChanged()
} }
@ -287,82 +258,41 @@ class PlaybackStateManager private constructor() {
*/ */
@Synchronized @Synchronized
fun addToQueue(songs: List<Song>) { fun addToQueue(songs: List<Song>) {
_queue.addAll(songs) queue.addToQueue(songs)
notifyQueueChanged() notifyQueueChanged()
} }
/** /**
* Move a [Song] in the queue. * Move a [Song] in the queue.
* @param from The position of the [Song] to move in the queue. * @param src The position of the [Song] to move in the queue.
* @param to The destination position in the queue. * @param dst The destination position in the queue.
*/ */
@Synchronized @Synchronized
fun moveQueueItem(from: Int, to: Int) { fun moveQueueItem(src: Int, dst: Int) {
logD("Moving item $from to position $to") logD("Moving item $src to position $dst")
_queue.add(to, _queue.removeAt(from)) queue.move(src, dst)
notifyQueueChanged() notifyQueueChanged()
} }
/** /**
* Remove a [Song] from the queue. * Remove a [Song] from the queue.
* @param index The position of the [Song] to remove in the queue. * @param at The position of the [Song] to remove in the queue.
*/ */
@Synchronized @Synchronized
fun removeQueueItem(index: Int) { fun removeQueueItem(at: Int) {
logD("Removing item ${_queue[index].rawName}") logD("Removing item at $at")
_queue.removeAt(index) queue.remove(at)
notifyQueueChanged() notifyQueueChanged()
} }
/** /**
* (Re)shuffle or (Re)order this instance. * (Re)shuffle or (Re)order this instance.
* @param shuffled Whether to shuffle the queue or not. * @param shuffled Whether to shuffle the queue or not.
* @param settings [Settings] required to configure the queue.
*/ */
@Synchronized @Synchronized
fun reshuffle(shuffled: Boolean, settings: Settings) { fun reorder(shuffled: Boolean) {
val song = song ?: return queue.reorder(shuffled)
orderQueue(settings, shuffled, song)
notifyQueueReworked() notifyQueueReworked()
notifyShuffledChanged()
}
/**
* Re-configure the queue.
* @param settings [Settings] required to configure the queue.
* @param shuffled Whether to shuffle the queue or not.
* @param keep the [Song] to start at in the new queue, or null if not specified.
*/
private fun orderQueue(settings: Settings, shuffled: Boolean, keep: Song?) {
val newIndex: Int
if (shuffled) {
// Shuffling queue, randomize the current song list and move the Song to play
// to the start.
_queue.shuffle()
if (keep != null) {
_queue.add(0, _queue.removeAt(_queue.indexOf(keep)))
}
newIndex = 0
} else {
// Ordering queue, re-sort it using the analogous parent sort configuration and
// then jump to the Song to play.
// TODO: Rework queue system to avoid having to do this
val sort =
parent.let { parent ->
when (parent) {
null -> settings.libSongSort
is Album -> settings.detailAlbumSort
is Artist -> settings.detailArtistSort
is Genre -> settings.detailGenreSort
}
}
sort.songsInPlace(_queue)
newIndex = keep?.let(_queue::indexOf) ?: 0
}
_queue = queue
index = newIndex
isShuffled = shuffled
} }
// --- INTERNAL PLAYER FUNCTIONS --- // --- INTERNAL PLAYER FUNCTIONS ---
@ -379,7 +309,7 @@ class PlaybackStateManager private constructor() {
return return
} }
val newState = internalPlayer.getState(song?.durationMs ?: 0) val newState = internalPlayer.getState(queue.currentSong?.durationMs ?: 0)
if (newState != playerState) { if (newState != playerState) {
playerState = newState playerState = newState
notifyStateChanged() notifyStateChanged()
@ -452,44 +382,49 @@ class PlaybackStateManager private constructor() {
return false return false
} }
val library = musicStore.library ?: return false // TODO: Re-implement with new queue
val internalPlayer = internalPlayer ?: return false
val state =
try {
withContext(Dispatchers.IO) { database.read(library) }
} catch (e: Exception) {
logE("Unable to restore playback state.")
logE(e.stackTraceToString())
return false return false
}
// Translate the state we have just read into a usable playback state for this // val library = musicStore.library ?: return false
// instance. // val internalPlayer = internalPlayer ?: return false
return synchronized(this) { // val state =
// State could have changed while we were loading, so check if we were initialized // try {
// now before applying the state. // withContext(Dispatchers.IO) { database.read(library) }
if (state != null && (!isInitialized || force)) { // } catch (e: Exception) {
index = state.index // logE("Unable to restore playback state.")
parent = state.parent // logE(e.stackTraceToString())
_queue = state.queue.toMutableList() // return false
repeatMode = state.repeatMode // }
isShuffled = state.isShuffled //
// // Translate the state we have just read into a usable playback state for this
notifyNewPlayback() // // instance.
notifyRepeatModeChanged() // return synchronized(this) {
notifyShuffledChanged() // // State could have changed while we were loading, so check if we were
// initialized
// Continuing playback after drastic state updates is a bad idea, so pause. // // now before applying the state.
internalPlayer.loadSong(song, false) // if (state != null && (!isInitialized || force)) {
internalPlayer.seekTo(state.positionMs) // index = state.index
// parent = state.parent
isInitialized = true // _queue = state.queue.toMutableList()
// repeatMode = state.repeatMode
true // isShuffled = state.isShuffled
} else { //
false // notifyNewPlayback()
} // notifyRepeatModeChanged()
} // notifyShuffledChanged()
//
// // Continuing playback after drastic state updates is a bad idea, so
// pause.
// internalPlayer.loadSong(song, false)
// internalPlayer.seekTo(state.positionMs)
//
// isInitialized = true
//
// true
// } else {
// false
// }
// }
} }
/** /**
@ -499,26 +434,26 @@ class PlaybackStateManager private constructor() {
*/ */
suspend fun saveState(database: PlaybackStateDatabase): Boolean { suspend fun saveState(database: PlaybackStateDatabase): Boolean {
logD("Saving state to DB") logD("Saving state to DB")
return false
// Create the saved state from the current playback state. // // Create the saved state from the current playback state.
val state = // val state =
synchronized(this) { // synchronized(this) {
PlaybackStateDatabase.SavedState( // PlaybackStateDatabase.SavedState(
index = index, // index = index,
parent = parent, // parent = parent,
queue = _queue, // queue = _queue,
positionMs = playerState.calculateElapsedPositionMs(), // positionMs = playerState.calculateElapsedPositionMs(),
isShuffled = isShuffled, // isShuffled = isShuffled,
repeatMode = repeatMode) // repeatMode = repeatMode)
} // }
return try { // return try {
withContext(Dispatchers.IO) { database.write(state) } // withContext(Dispatchers.IO) { database.write(state) }
true // true
} catch (e: Exception) { // } catch (e: Exception) {
logE("Unable to save playback state.") // logE("Unable to save playback state.")
logE(e.stackTraceToString()) // logE(e.stackTraceToString())
false // false
} // }
} }
/** /**
@ -543,54 +478,57 @@ class PlaybackStateManager private constructor() {
*/ */
@Synchronized @Synchronized
fun sanitize(newLibrary: MusicStore.Library) { fun sanitize(newLibrary: MusicStore.Library) {
if (!isInitialized) { // if (!isInitialized) {
// Nothing playing, nothing to do. // // Nothing playing, nothing to do.
logD("Not initialized, no need to sanitize") // logD("Not initialized, no need to sanitize")
return // return
} // }
//
val internalPlayer = internalPlayer ?: return // val internalPlayer = internalPlayer ?: return
//
logD("Sanitizing state") // logD("Sanitizing state")
//
// While we could just save and reload the state, we instead sanitize the state // // While we could just save and reload the state, we instead sanitize the state
// at runtime for better performance (and to sidestep a co-routine on behalf of the caller). // // at runtime for better performance (and to sidestep a co-routine on behalf of
// the caller).
// Sanitize parent //
parent = // // Sanitize parent
parent?.let { // parent =
when (it) { // parent?.let {
is Album -> newLibrary.sanitize(it) // when (it) {
is Artist -> newLibrary.sanitize(it) // is Album -> newLibrary.sanitize(it)
is Genre -> newLibrary.sanitize(it) // is Artist -> newLibrary.sanitize(it)
} // is Genre -> newLibrary.sanitize(it)
} // }
// }
// Sanitize queue. Make sure we re-align the index to point to the previously playing //
// Song in the queue queue. // // Sanitize queue. Make sure we re-align the index to point to the previously
val oldSongUid = song?.uid // playing
_queue = _queue.mapNotNullTo(mutableListOf()) { newLibrary.sanitize(it) } // // Song in the queue queue.
while (song?.uid != oldSongUid && index > -1) { // val oldSongUid = song?.uid
index-- // _queue = _queue.mapNotNullTo(mutableListOf()) { newLibrary.sanitize(it) }
} // while (song?.uid != oldSongUid && index > -1) {
// index--
notifyNewPlayback() // }
//
val oldPosition = playerState.calculateElapsedPositionMs() // notifyNewPlayback()
// Continuing playback while also possibly doing drastic state updates is //
// a bad idea, so pause. // val oldPosition = playerState.calculateElapsedPositionMs()
internalPlayer.loadSong(song, false) // // Continuing playback while also possibly doing drastic state updates is
if (index > -1) { // // a bad idea, so pause.
// Internal player may have reloaded the media item, re-seek to the previous position // internalPlayer.loadSong(song, false)
seekTo(oldPosition) // if (index > -1) {
} // // Internal player may have reloaded the media item, re-seek to the previous
// position
// seekTo(oldPosition)
// }
} }
// --- CALLBACKS --- // --- CALLBACKS ---
private fun notifyIndexMoved() { private fun notifyIndexMoved() {
for (callback in listeners) { for (callback in listeners) {
callback.onIndexMoved(index) callback.onIndexMoved(queue)
} }
} }
@ -602,13 +540,13 @@ class PlaybackStateManager private constructor() {
private fun notifyQueueReworked() { private fun notifyQueueReworked() {
for (callback in listeners) { for (callback in listeners) {
callback.onQueueReworked(index, queue) callback.onQueueReworked(queue)
} }
} }
private fun notifyNewPlayback() { private fun notifyNewPlayback() {
for (callback in listeners) { for (callback in listeners) {
callback.onNewPlayback(index, queue, parent) callback.onNewPlayback(queue, parent)
} }
} }
@ -624,12 +562,6 @@ class PlaybackStateManager private constructor() {
} }
} }
private fun notifyShuffledChanged() {
for (callback in listeners) {
callback.onShuffledChanged(isShuffled)
}
}
/** /**
* The interface for receiving updates from [PlaybackStateManager]. Add the listener to * The interface for receiving updates from [PlaybackStateManager]. Add the listener to
* [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener]. * [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener].
@ -638,30 +570,29 @@ class PlaybackStateManager private constructor() {
/** /**
* Called when the position of the currently playing item has changed, changing the current * Called when the position of the currently playing item has changed, changing the current
* [Song], but no other queue attribute has changed. * [Song], but no other queue attribute has changed.
* @param index The new position in the queue. * @param queue The new [Queue].
*/ */
fun onIndexMoved(index: Int) {} fun onIndexMoved(queue: Queue) {}
/** /**
* Called when the queue changed in a trivial manner, such as a move. * Called when the [Queue] changed in a trivial manner, such as a move.
* @param queue The new queue. * @param queue The new [Queue].
*/ */
fun onQueueChanged(queue: List<Song>) {} fun onQueueChanged(queue: Queue) {}
/** /**
* Called when the queue has changed in a non-trivial manner (such as re-shuffling), but the * Called when the [Queue] has changed in a non-trivial manner (such as re-shuffling), but
* currently playing [Song] has not. * the currently playing [Song] has not.
* @param index The new position in the queue. * @param queue The new [Queue].
*/ */
fun onQueueReworked(index: Int, queue: List<Song>) {} fun onQueueReworked(queue: Queue) {}
/** /**
* Called when a new playback configuration was created. * Called when a new playback configuration was created.
* @param index The new position in the queue. * @param queue The new [Queue].
* @param queue The new queue.
* @param parent The new [MusicParent] being played from, or null if playing from all songs. * @param parent The new [MusicParent] being played from, or null if playing from all songs.
*/ */
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {} fun onNewPlayback(queue: Queue, parent: MusicParent?) {}
/** /**
* Called when the state of the [InternalPlayer] changes. * Called when the state of the [InternalPlayer] changes.
@ -674,13 +605,6 @@ class PlaybackStateManager private constructor() {
* @param repeatMode The new [RepeatMode]. * @param repeatMode The new [RepeatMode].
*/ */
fun onRepeatChanged(repeatMode: RepeatMode) {} fun onRepeatChanged(repeatMode: RepeatMode) {}
/**
* Called when the queue's shuffle state changes. Handling the queue change itself should
* occur in [onQueueReworked],
* @param isShuffled Whether the queue is shuffled.
*/
fun onShuffledChanged(isShuffled: Boolean) {}
} }
companion object { companion object {

View file

@ -0,0 +1,235 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.state
import kotlin.random.Random
import kotlin.random.nextInt
import org.oxycblt.auxio.music.Song
/**
* 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 ot be known for this data structure to be used.
* The functions exposed should be familiar for any typical play queue.
*
* @author OxygenCobalt
*/
class Queue {
@Volatile private var heap = mutableListOf<Song>()
@Volatile private var orderedMapping = mutableListOf<Int>()
@Volatile private var shuffledMapping = mutableListOf<Int>()
/** The index of the currently playing [Song] in the current mapping. */
@Volatile
var index = 0
private set
/** The currently playing [Song]. */
val currentSong: Song?
get() = shuffledMapping.ifEmpty { orderedMapping.ifEmpty { null } }?.let { heap[it[index]] }
/** Whether this queue is shuffled. */
val isShuffled: Boolean
get() = shuffledMapping.isNotEmpty()
/**
* Resolve this queue into a more conventional list of [Song]s.
* @return A list of [Song] corresponding to the current queue mapping.
*/
fun resolve() = shuffledMapping.map { heap[it] }.ifEmpty { orderedMapping.map { heap[it] } }
/**
* 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 song The [Song] to play, or null to start from a random position.
* @param queue The queue of [Song]s to play. Must contain [song]. This list will become the
* heap internally.
* @param shuffled Whether to shuffle the queue or not. This changes the interpretation of
* [queue].
*/
fun start(song: Song?, queue: List<Song>, shuffled: Boolean) {
heap = queue.toMutableList()
orderedMapping = MutableList(queue.size) { it }
shuffledMapping = mutableListOf()
index =
song?.let(queue::indexOf) ?: if (shuffled) Random.Default.nextInt(queue.indices) else 0
reorder(shuffled)
}
/**
* Re-order the queue.
* @param shuffled Whether the queue should be shuffled or not.
*/
fun reorder(shuffled: Boolean) {
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 = MutableList(orderedMapping.size) { it }.apply { shuffle() }
shuffledMapping.add(0, shuffledMapping.removeAt(shuffledMapping.indexOf(trueIndex)))
index = 0
} else if (shuffledMapping.isNotEmpty()) {
// Un-shuffling, song to preserve is in the shuffled mapping.
index = orderedMapping.indexOf(shuffledMapping[index])
shuffledMapping = mutableListOf()
}
}
/**
* Add [Song]s to the top of the queue. Will start playback if nothing is playing.
* @param songs The [Song]s to add.
*/
fun playNext(songs: List<Song>) {
if (orderedMapping.isEmpty()) {
// No playback, start playing these songs.
start(songs[0], songs, false)
return
}
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.
val orderedIndex = orderedMapping.indexOf(shuffledMapping[index])
orderedMapping.addAll(orderedIndex, heapIndices)
shuffledMapping.addAll(index, heapIndices)
} else {
// Add the new song in front of the current index in the ordered mapping.
orderedMapping.addAll(index, heapIndices)
}
}
/**
* Add [Song]s to the end of the queue. Will start playback if nothing is playing.
* @param songs The [Song]s to add.
*/
fun addToQueue(songs: List<Song>) {
if (orderedMapping.isEmpty()) {
// No playback, start playing these songs.
start(songs[0], songs, false)
return
}
val heapIndices = songs.map(::addSongToHeap)
// Can simple append the new songs to the end of both mappings.
orderedMapping.addAll(heapIndices)
if (shuffledMapping.isNotEmpty()) {
shuffledMapping.addAll(heapIndices)
}
}
/**
* 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].
*/
fun move(src: Int, dst: Int) {
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))
}
if (index in (src + 1) until dst) {
// Index was ahead of moved song but not ahead of it's destination position.
// This makes it functionally a removal, so update the index to preserve consistency.
index -= 1
} else if (index == src) {
// Moving the currently playing song.
index = dst
}
}
/**
* Remove a [Song] at the given position.
* @param at The position of the [Song] to remove.
*/
fun remove(at: Int) {
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 spe
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.
if (index > at) {
// Index was ahead of removed song, shift back to preserve consistency.
index -= 1
}
}
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)
}
}
orphanCandidates.removeAll(currentMapping.toSet())
if (orphanCandidates.isNotEmpty()) {
// There are orphaned songs, return the first one we find.
return orphanCandidates.first()
}
}
// Nothing to re-use, add this song to the queue
heap.add(song)
return heap.lastIndex
}
}

View file

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

View file

@ -36,6 +36,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.ActionMode import org.oxycblt.auxio.playback.ActionMode
import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.InternalPlayer
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.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -92,22 +93,29 @@ class MediaSessionComponent(private val context: Context, private val listener:
// --- PLAYBACKSTATEMANAGER OVERRIDES --- // --- PLAYBACKSTATEMANAGER OVERRIDES ---
override fun onIndexMoved(index: Int) { override fun onIndexMoved(queue: Queue) {
updateMediaMetadata(playbackManager.song, playbackManager.parent) updateMediaMetadata(playbackManager.queue.currentSong, playbackManager.parent)
invalidateSessionState() invalidateSessionState()
} }
override fun onQueueChanged(queue: List<Song>) { override fun onQueueChanged(queue: Queue) {
updateQueue(queue) updateQueue(queue)
} }
override fun onQueueReworked(index: Int, queue: List<Song>) { override fun onQueueReworked(queue: Queue) {
updateQueue(queue) updateQueue(queue)
invalidateSessionState() invalidateSessionState()
mediaSession.setShuffleMode(
if (queue.isShuffled) {
PlaybackStateCompat.SHUFFLE_MODE_ALL
} else {
PlaybackStateCompat.SHUFFLE_MODE_NONE
})
invalidateSecondaryAction()
} }
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) { override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
updateMediaMetadata(playbackManager.song, parent) updateMediaMetadata(playbackManager.queue.currentSong, parent)
updateQueue(queue) updateQueue(queue)
invalidateSessionState() invalidateSessionState()
} }
@ -131,23 +139,12 @@ class MediaSessionComponent(private val context: Context, private val listener:
invalidateSecondaryAction() invalidateSecondaryAction()
} }
override fun onShuffledChanged(isShuffled: Boolean) {
mediaSession.setShuffleMode(
if (isShuffled) {
PlaybackStateCompat.SHUFFLE_MODE_ALL
} else {
PlaybackStateCompat.SHUFFLE_MODE_NONE
})
invalidateSecondaryAction()
}
// --- SETTINGS OVERRIDES --- // --- SETTINGS OVERRIDES ---
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) { when (key) {
context.getString(R.string.set_key_cover_mode) -> context.getString(R.string.set_key_cover_mode) ->
updateMediaMetadata(playbackManager.song, playbackManager.parent) updateMediaMetadata(playbackManager.queue.currentSong, playbackManager.parent)
context.getString(R.string.set_key_notif_action) -> invalidateSecondaryAction() context.getString(R.string.set_key_notif_action) -> invalidateSecondaryAction()
} }
} }
@ -219,17 +216,14 @@ class MediaSessionComponent(private val context: Context, private val listener:
} }
override fun onSetShuffleMode(shuffleMode: Int) { override fun onSetShuffleMode(shuffleMode: Int) {
playbackManager.reshuffle( playbackManager.reorder(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP, shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP)
settings)
} }
override fun onSkipToQueueItem(id: Long) { override fun onSkipToQueueItem(id: Long) {
if (id in playbackManager.queue.indices) {
playbackManager.goto(id.toInt()) playbackManager.goto(id.toInt())
} }
}
override fun onCustomAction(action: String?, extras: Bundle?) { override fun onCustomAction(action: String?, extras: Bundle?) {
super.onCustomAction(action, extras) super.onCustomAction(action, extras)
@ -318,9 +312,9 @@ class MediaSessionComponent(private val context: Context, private val listener:
* Upload a new queue to the [MediaSessionCompat]. * Upload a new queue to the [MediaSessionCompat].
* @param queue The current queue to upload. * @param queue The current queue to upload.
*/ */
private fun updateQueue(queue: List<Song>) { private fun updateQueue(queue: Queue) {
val queueItems = val queueItems =
queue.mapIndexed { i, song -> queue.resolve().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,
@ -350,7 +344,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
.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(playbackManager.index.toLong()) .setActiveQueueItemId(playbackManager.queue.index.toLong())
// Android 13+ relies on custom actions in the notification. // Android 13+ relies on custom actions in the notification.
@ -361,7 +355,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
PlaybackStateCompat.CustomAction.Builder( PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INVERT_SHUFFLE, PlaybackService.ACTION_INVERT_SHUFFLE,
context.getString(R.string.desc_shuffle), context.getString(R.string.desc_shuffle),
if (playbackManager.isShuffled) { if (playbackManager.queue.isShuffled) {
R.drawable.ic_shuffle_on_24 R.drawable.ic_shuffle_on_24
} else { } else {
R.drawable.ic_shuffle_off_24 R.drawable.ic_shuffle_off_24
@ -391,7 +385,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
invalidateSessionState() invalidateSessionState()
when (settings.playbackNotificationAction) { when (settings.playbackNotificationAction) {
ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.isShuffled) ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.queue.isShuffled)
else -> notification.updateRepeatMode(playbackManager.repeatMode) else -> notification.updateRepeatMode(playbackManager.repeatMode)
} }

View file

@ -351,12 +351,16 @@ class PlaybackService :
} }
// Shuffle all -> Start new playback from all songs // Shuffle all -> Start new playback from all songs
is InternalPlayer.Action.ShuffleAll -> { is InternalPlayer.Action.ShuffleAll -> {
playbackManager.play(null, null, settings, true) playbackManager.play(null, null, settings.libSongSort, true)
} }
// Open -> Try to find the Song for the given file and then play it from all songs // Open -> Try to find the Song for the given file and then play it from all songs
is InternalPlayer.Action.Open -> { is InternalPlayer.Action.Open -> {
library.findSongForUri(application, action.uri)?.let { song -> library.findSongForUri(application, action.uri)?.let { song ->
playbackManager.play(song, null, settings) playbackManager.play(
song,
null,
settings.libSongSort,
playbackManager.queue.isShuffled && settings.keepShuffle)
} }
} }
} }
@ -411,8 +415,7 @@ class PlaybackService :
playbackManager.setPlaying(!playbackManager.playerState.isPlaying) playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
ACTION_INC_REPEAT_MODE -> ACTION_INC_REPEAT_MODE ->
playbackManager.repeatMode = playbackManager.repeatMode.increment() playbackManager.repeatMode = playbackManager.repeatMode.increment()
ACTION_INVERT_SHUFFLE -> ACTION_INVERT_SHUFFLE -> playbackManager.reorder(!playbackManager.queue.isShuffled)
playbackManager.reshuffle(!playbackManager.isShuffled, settings)
ACTION_SKIP_PREV -> playbackManager.prev() ACTION_SKIP_PREV -> playbackManager.prev()
ACTION_SKIP_NEXT -> playbackManager.next() ACTION_SKIP_NEXT -> playbackManager.next()
ACTION_EXIT -> { ACTION_EXIT -> {
@ -428,7 +431,7 @@ class PlaybackService :
// which would result in unexpected playback. Work around it by dropping the first // which would result in unexpected playback. Work around it by dropping the first
// call to this function, which should come from that Intent. // call to this function, which should come from that Intent.
if (settings.headsetAutoplay && if (settings.headsetAutoplay &&
playbackManager.song != null && playbackManager.queue.currentSong != null &&
initialHeadsetPlugEventHandled) { initialHeadsetPlugEventHandled) {
logD("Device connected, resuming") logD("Device connected, resuming")
playbackManager.setPlaying(true) playbackManager.setPlaying(true)
@ -436,7 +439,7 @@ class PlaybackService :
} }
private fun pauseFromHeadsetPlug() { private fun pauseFromHeadsetPlug() {
if (playbackManager.song != null) { if (playbackManager.queue.currentSong != null) {
logD("Device disconnected, pausing") logD("Device disconnected, pausing")
playbackManager.setPlaying(false) playbackManager.setPlaying(false)
} }

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.InternalPlayer
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.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getDimenPixels
@ -55,7 +56,7 @@ class WidgetComponent(private val context: Context) :
/** Update [WidgetProvider] with the current playback state. */ /** Update [WidgetProvider] with the current playback state. */
fun update() { fun update() {
val song = playbackManager.song val song = playbackManager.queue.currentSong
if (song == null) { if (song == null) {
logD("No song, resetting widget") logD("No song, resetting widget")
widgetProvider.update(context, null) widgetProvider.update(context, null)
@ -65,7 +66,7 @@ class WidgetComponent(private val context: Context) :
// Note: Store these values here so they remain consistent once the bitmap is loaded. // Note: Store these values here so they remain consistent once the bitmap is loaded.
val isPlaying = playbackManager.playerState.isPlaying val isPlaying = playbackManager.playerState.isPlaying
val repeatMode = playbackManager.repeatMode val repeatMode = playbackManager.repeatMode
val isShuffled = playbackManager.isShuffled val isShuffled = playbackManager.queue.isShuffled
provider.load( provider.load(
song, song,
@ -115,10 +116,10 @@ class WidgetComponent(private val context: Context) :
// Hook all the major song-changing updates + the major player state updates // Hook all the major song-changing updates + the major player state updates
// to updating the "Now Playing" widget. // to updating the "Now Playing" widget.
override fun onIndexMoved(index: Int) = update() override fun onIndexMoved(queue: Queue) = update()
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) = update() override fun onQueueReworked(queue: Queue) = update()
override fun onNewPlayback(queue: Queue, parent: MusicParent?) = update()
override fun onStateChanged(state: InternalPlayer.State) = update() override fun onStateChanged(state: InternalPlayer.State) = update()
override fun onShuffledChanged(isShuffled: Boolean) = update()
override fun onRepeatChanged(repeatMode: RepeatMode) = update() override fun onRepeatChanged(repeatMode: RepeatMode) = update()
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {