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:
parent
1b44eeae15
commit
16513e6547
12 changed files with 501 additions and 349 deletions
|
@ -30,9 +30,9 @@ import kotlinx.parcelize.IgnoredOnParcel
|
|||
import kotlinx.parcelize.Parcelize
|
||||
import org.oxycblt.auxio.R
|
||||
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.parseMultiValue
|
||||
import org.oxycblt.auxio.music.storage.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
|
|
@ -29,6 +29,7 @@ import androidx.core.database.getStringOrNull
|
|||
import java.io.File
|
||||
import org.oxycblt.auxio.music.Date
|
||||
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.contentResolverSafe
|
||||
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.storageVolumesCompat
|
||||
import org.oxycblt.auxio.music.storage.useQuery
|
||||
import org.oxycblt.auxio.music.parsing.parseId3v2Position
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
|
|
@ -23,8 +23,8 @@ import com.google.android.exoplayer2.MediaItem
|
|||
import com.google.android.exoplayer2.MetadataRetriever
|
||||
import org.oxycblt.auxio.music.Date
|
||||
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.storage.toAudioUri
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
|
|
|
@ -26,10 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
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.playback.state.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
||||
|
@ -100,13 +97,18 @@ class PlaybackViewModel(application: Application) :
|
|||
playbackManager.removeListener(this)
|
||||
}
|
||||
|
||||
override fun onIndexMoved(index: Int) {
|
||||
_song.value = playbackManager.song
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
_song.value = playbackManager.queue.currentSong
|
||||
}
|
||||
|
||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||
_song.value = playbackManager.song
|
||||
override fun onQueueReworked(queue: Queue) {
|
||||
_isShuffled.value = queue.isShuffled
|
||||
}
|
||||
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||
_song.value = playbackManager.queue.currentSong
|
||||
_parent.value = playbackManager.parent
|
||||
_isShuffled.value = queue.isShuffled
|
||||
}
|
||||
|
||||
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) {
|
||||
_repeatMode.value = repeatMode
|
||||
}
|
||||
|
@ -141,21 +139,19 @@ class PlaybackViewModel(application: Application) :
|
|||
* @param song The [Song] to play.
|
||||
*/
|
||||
fun playFromAll(song: Song) {
|
||||
playbackManager.play(song, null, settings)
|
||||
playImpl(song, null)
|
||||
}
|
||||
|
||||
/** Shuffle all songs in the music library. */
|
||||
fun shuffleAll() {
|
||||
playbackManager.play(null, null, settings, true)
|
||||
playImpl(null, null, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a [Song] from it's [Album].
|
||||
* @param song The [Song] to play.
|
||||
*/
|
||||
fun playFromAlbum(song: Song) {
|
||||
playbackManager.play(song, song.album, settings)
|
||||
}
|
||||
fun playFromAlbum(song: Song) = playImpl(song, song.album)
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (artist != null) {
|
||||
check(artist in song.artists) { "Artist not in song artists" }
|
||||
playbackManager.play(song, artist, settings)
|
||||
playImpl(song, artist)
|
||||
} else if (song.artists.size == 1) {
|
||||
playbackManager.play(song, song.artists[0], settings)
|
||||
playImpl(song, song.artists[0])
|
||||
} else {
|
||||
_artistPlaybackPickerSong.value = song
|
||||
}
|
||||
|
@ -191,10 +186,9 @@ class PlaybackViewModel(application: Application) :
|
|||
*/
|
||||
fun playFromGenre(song: Song, genre: Genre? = null) {
|
||||
if (genre != null) {
|
||||
check(genre.songs.contains(song)) { "Invalid input: Genre is not linked to song" }
|
||||
playbackManager.play(song, genre, settings)
|
||||
playImpl(song, genre)
|
||||
} else if (song.genres.size == 1) {
|
||||
playbackManager.play(song, song.genres[0], settings)
|
||||
playImpl(song, song.genres[0])
|
||||
} else {
|
||||
_genrePlaybackPickerSong.value = song
|
||||
}
|
||||
|
@ -204,49 +198,37 @@ class PlaybackViewModel(application: Application) :
|
|||
* Play an [Album].
|
||||
* @param album The [Album] to play.
|
||||
*/
|
||||
fun play(album: Album) {
|
||||
playbackManager.play(null, album, settings, false)
|
||||
}
|
||||
fun play(album: Album) = playImpl(null, album, false)
|
||||
|
||||
/**
|
||||
* Play an [Artist].
|
||||
* @param artist The [Artist] to play.
|
||||
*/
|
||||
fun play(artist: Artist) {
|
||||
playbackManager.play(null, artist, settings, false)
|
||||
}
|
||||
fun play(artist: Artist) = playImpl(null, artist, false)
|
||||
|
||||
/**
|
||||
* Play a [Genre].
|
||||
* @param genre The [Genre] to play.
|
||||
*/
|
||||
fun play(genre: Genre) {
|
||||
playbackManager.play(null, genre, settings, false)
|
||||
}
|
||||
fun play(genre: Genre) = playImpl(null, genre, false)
|
||||
|
||||
/**
|
||||
* Shuffle an [Album].
|
||||
* @param album The [Album] to shuffle.
|
||||
*/
|
||||
fun shuffle(album: Album) {
|
||||
playbackManager.play(null, album, settings, true)
|
||||
}
|
||||
fun shuffle(album: Album) = playImpl(null, album, true)
|
||||
|
||||
/**
|
||||
* Shuffle an [Artist].
|
||||
* @param artist The [Artist] to shuffle.
|
||||
*/
|
||||
fun shuffle(artist: Artist) {
|
||||
playbackManager.play(null, artist, settings, true)
|
||||
}
|
||||
fun shuffle(artist: Artist) = playImpl(null, artist, true)
|
||||
|
||||
/**
|
||||
* Shuffle an [Genre].
|
||||
* @param genre The [Genre] to shuffle.
|
||||
*/
|
||||
fun shuffle(genre: Genre) {
|
||||
playbackManager.play(null, genre, settings, true)
|
||||
}
|
||||
fun shuffle(genre: Genre) = playImpl(null, genre, true)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
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 ---
|
||||
|
||||
/**
|
||||
|
@ -370,7 +370,7 @@ class PlaybackViewModel(application: Application) :
|
|||
|
||||
/** Toggle [isShuffled] (ex. from on to off) */
|
||||
fun invertShuffled() {
|
||||
playbackManager.reshuffle(!playbackManager.isShuffled, settings)
|
||||
playbackManager.reorder(!playbackManager.queue.isShuffled)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
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.
|
||||
|
@ -36,7 +37,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
|
|||
/** The current 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. */
|
||||
val index: StateFlow<Int>
|
||||
get() = _index
|
||||
|
@ -56,10 +57,6 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
|
|||
* range.
|
||||
*/
|
||||
fun goto(adapterIndex: Int) {
|
||||
if (adapterIndex !in playbackManager.queue.indices) {
|
||||
// Invalid input. Nothing to do.
|
||||
return
|
||||
}
|
||||
playbackManager.goto(adapterIndex)
|
||||
}
|
||||
|
||||
|
@ -69,10 +66,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
|
|||
* range.
|
||||
*/
|
||||
fun removeQueueDataItem(adapterIndex: Int) {
|
||||
if (adapterIndex <= playbackManager.index ||
|
||||
adapterIndex !in playbackManager.queue.indices) {
|
||||
// Invalid input. Nothing to do.
|
||||
// TODO: Allow editing played queue items.
|
||||
if (adapterIndex !in queue.value.indices) {
|
||||
return
|
||||
}
|
||||
playbackManager.removeQueueItem(adapterIndex)
|
||||
|
@ -85,7 +79,8 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
|
|||
* @return true if the items were moved, false otherwise.
|
||||
*/
|
||||
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.
|
||||
return false
|
||||
}
|
||||
|
@ -103,34 +98,34 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
|
|||
scrollTo = null
|
||||
}
|
||||
|
||||
override fun onIndexMoved(index: Int) {
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
// Index moved -> Scroll to new index
|
||||
replaceQueue = null
|
||||
scrollTo = index
|
||||
_index.value = index
|
||||
scrollTo = queue.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.
|
||||
replaceQueue = false
|
||||
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
|
||||
replaceQueue = true
|
||||
scrollTo = index
|
||||
_queue.value = playbackManager.queue.toMutableList()
|
||||
_index.value = index
|
||||
scrollTo = queue.index
|
||||
_queue.value = queue.resolve()
|
||||
_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
|
||||
replaceQueue = true
|
||||
scrollTo = index
|
||||
_queue.value = playbackManager.queue.toMutableList()
|
||||
_index.value = index
|
||||
scrollTo = queue.index
|
||||
_queue.value = queue.resolve()
|
||||
_index.value = queue.index
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
|
@ -133,7 +133,7 @@ class ReplayGainAudioProcessor(private val context: Context) :
|
|||
// User wants album gain to be used when in an album, track gain otherwise.
|
||||
ReplayGainMode.DYNAMIC ->
|
||||
playbackManager.parent is Album &&
|
||||
playbackManager.song?.album == playbackManager.parent
|
||||
playbackManager.queue.currentSong?.album == playbackManager.parent
|
||||
}
|
||||
|
||||
val resolvedGain =
|
||||
|
|
|
@ -17,18 +17,11 @@
|
|||
|
||||
package org.oxycblt.auxio.playback.state
|
||||
|
||||
import kotlin.math.max
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.music.Album
|
||||
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.music.*
|
||||
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.logE
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
@ -59,22 +52,13 @@ class PlaybackStateManager private constructor() {
|
|||
@Volatile private var pendingAction: InternalPlayer.Action? = null
|
||||
@Volatile private var isInitialized = false
|
||||
|
||||
/** The currently playing [Song]. Null if nothing is playing. */
|
||||
val song
|
||||
get() = queue.getOrNull(index)
|
||||
/** The current [Queue]. */
|
||||
val queue = Queue()
|
||||
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
||||
@Volatile
|
||||
var parent: MusicParent? = null
|
||||
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. */
|
||||
@Volatile
|
||||
var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
|
||||
|
@ -86,13 +70,8 @@ class PlaybackStateManager private constructor() {
|
|||
field = value
|
||||
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
|
||||
* available.
|
||||
* The current audio session ID of the internal player. Null if [InternalPlayer] is unavailable.
|
||||
*/
|
||||
val currentAudioSessionId: Int?
|
||||
get() = internalPlayer?.audioSessionId
|
||||
|
@ -106,9 +85,8 @@ class PlaybackStateManager private constructor() {
|
|||
@Synchronized
|
||||
fun addListener(listener: Listener) {
|
||||
if (isInitialized) {
|
||||
listener.onNewPlayback(index, queue, parent)
|
||||
listener.onNewPlayback(queue, parent)
|
||||
listener.onRepeatChanged(repeatMode)
|
||||
listener.onShuffledChanged(isShuffled)
|
||||
listener.onStateChanged(playerState)
|
||||
}
|
||||
|
||||
|
@ -141,7 +119,7 @@ class PlaybackStateManager private constructor() {
|
|||
}
|
||||
|
||||
if (isInitialized) {
|
||||
internalPlayer.loadSong(song, playerState.isPlaying)
|
||||
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
|
||||
internalPlayer.seekTo(playerState.calculateElapsedPositionMs())
|
||||
// See if there's any action that has been queued.
|
||||
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 parent The [MusicParent] to play from, or null if to play from the entire
|
||||
* [MusicStore.Library].
|
||||
* @param settings [Settings] required to configure the queue.
|
||||
* @param shuffled Whether to shuffle the queue. Defaults to the "Remember shuffle"
|
||||
* configuration.
|
||||
* @param sort [Sort] to initially sort an ordered queue with.
|
||||
* @param shuffled Whether to shuffle or not.
|
||||
*/
|
||||
@Synchronized
|
||||
fun play(
|
||||
song: Song?,
|
||||
parent: MusicParent?,
|
||||
settings: Settings,
|
||||
shuffled: Boolean = settings.keepShuffle && isShuffled
|
||||
) {
|
||||
fun play(song: Song?, parent: MusicParent?, sort: Sort, shuffled: Boolean) {
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
val library = musicStore.library ?: return
|
||||
// Setup parent and queue
|
||||
// Set up parent and queue
|
||||
this.parent = parent
|
||||
_queue = (parent?.songs ?: library.songs).toMutableList()
|
||||
orderQueue(settings, shuffled, song)
|
||||
queue.start(song, sort.songs(parent?.songs ?: library.songs), shuffled)
|
||||
// Notify components of changes
|
||||
notifyNewPlayback()
|
||||
notifyShuffledChanged()
|
||||
internalPlayer.loadSong(this.song, true)
|
||||
internalPlayer.loadSong(queue.currentSong, true)
|
||||
// Played something, so we are initialized now
|
||||
isInitialized = true
|
||||
}
|
||||
|
@ -209,13 +179,13 @@ class PlaybackStateManager private constructor() {
|
|||
@Synchronized
|
||||
fun next() {
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
// Increment the index, if it cannot be incremented any further, then
|
||||
// repeat and pause/resume playback depending on the setting
|
||||
if (index < _queue.lastIndex) {
|
||||
gotoImpl(internalPlayer, index + 1, true)
|
||||
} else {
|
||||
gotoImpl(internalPlayer, 0, repeatMode == RepeatMode.ALL)
|
||||
var play = true
|
||||
if (!queue.goto(queue.index + 1)) {
|
||||
queue.goto(0)
|
||||
play = false
|
||||
}
|
||||
notifyIndexMoved()
|
||||
internalPlayer.loadSong(queue.currentSong, play)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -231,7 +201,11 @@ class PlaybackStateManager private constructor() {
|
|||
rewind()
|
||||
setPlaying(true)
|
||||
} 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
|
||||
fun goto(index: Int) {
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
gotoImpl(internalPlayer, index, true)
|
||||
}
|
||||
|
||||
private fun gotoImpl(internalPlayer: InternalPlayer, idx: Int, play: Boolean) {
|
||||
index = idx
|
||||
notifyIndexMoved()
|
||||
internalPlayer.loadSong(song, play)
|
||||
if (queue.goto(index)) {
|
||||
notifyIndexMoved()
|
||||
internalPlayer.loadSong(queue.currentSong, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -257,7 +228,7 @@ class PlaybackStateManager private constructor() {
|
|||
*/
|
||||
@Synchronized
|
||||
fun playNext(song: Song) {
|
||||
_queue.add(index + 1, song)
|
||||
queue.playNext(listOf(song))
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
|
@ -267,7 +238,7 @@ class PlaybackStateManager private constructor() {
|
|||
*/
|
||||
@Synchronized
|
||||
fun playNext(songs: List<Song>) {
|
||||
_queue.addAll(index + 1, songs)
|
||||
queue.playNext(songs)
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
|
@ -277,7 +248,7 @@ class PlaybackStateManager private constructor() {
|
|||
*/
|
||||
@Synchronized
|
||||
fun addToQueue(song: Song) {
|
||||
_queue.add(song)
|
||||
queue.addToQueue(listOf(song))
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
|
@ -287,82 +258,41 @@ class PlaybackStateManager private constructor() {
|
|||
*/
|
||||
@Synchronized
|
||||
fun addToQueue(songs: List<Song>) {
|
||||
_queue.addAll(songs)
|
||||
queue.addToQueue(songs)
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a [Song] in the queue.
|
||||
* @param from The position of the [Song] to move in the queue.
|
||||
* @param to The destination position in the queue.
|
||||
* @param src The position of the [Song] to move in the queue.
|
||||
* @param dst The destination position in the queue.
|
||||
*/
|
||||
@Synchronized
|
||||
fun moveQueueItem(from: Int, to: Int) {
|
||||
logD("Moving item $from to position $to")
|
||||
_queue.add(to, _queue.removeAt(from))
|
||||
fun moveQueueItem(src: Int, dst: Int) {
|
||||
logD("Moving item $src to position $dst")
|
||||
queue.move(src, dst)
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
fun removeQueueItem(index: Int) {
|
||||
logD("Removing item ${_queue[index].rawName}")
|
||||
_queue.removeAt(index)
|
||||
fun removeQueueItem(at: Int) {
|
||||
logD("Removing item at $at")
|
||||
queue.remove(at)
|
||||
notifyQueueChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* (Re)shuffle or (Re)order this instance.
|
||||
* @param shuffled Whether to shuffle the queue or not.
|
||||
* @param settings [Settings] required to configure the queue.
|
||||
*/
|
||||
@Synchronized
|
||||
fun reshuffle(shuffled: Boolean, settings: Settings) {
|
||||
val song = song ?: return
|
||||
orderQueue(settings, shuffled, song)
|
||||
fun reorder(shuffled: Boolean) {
|
||||
queue.reorder(shuffled)
|
||||
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 ---
|
||||
|
@ -379,7 +309,7 @@ class PlaybackStateManager private constructor() {
|
|||
return
|
||||
}
|
||||
|
||||
val newState = internalPlayer.getState(song?.durationMs ?: 0)
|
||||
val newState = internalPlayer.getState(queue.currentSong?.durationMs ?: 0)
|
||||
if (newState != playerState) {
|
||||
playerState = newState
|
||||
notifyStateChanged()
|
||||
|
@ -452,44 +382,49 @@ class PlaybackStateManager private constructor() {
|
|||
return false
|
||||
}
|
||||
|
||||
val library = musicStore.library ?: return false
|
||||
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
|
||||
}
|
||||
// TODO: Re-implement with new queue
|
||||
return false
|
||||
|
||||
// Translate the state we have just read into a usable playback state for this
|
||||
// instance.
|
||||
return synchronized(this) {
|
||||
// State could have changed while we were loading, so check if we were initialized
|
||||
// now before applying the state.
|
||||
if (state != null && (!isInitialized || force)) {
|
||||
index = state.index
|
||||
parent = state.parent
|
||||
_queue = state.queue.toMutableList()
|
||||
repeatMode = state.repeatMode
|
||||
isShuffled = state.isShuffled
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
// val library = musicStore.library ?: return false
|
||||
// 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
|
||||
// }
|
||||
//
|
||||
// // Translate the state we have just read into a usable playback state for this
|
||||
// // instance.
|
||||
// return synchronized(this) {
|
||||
// // State could have changed while we were loading, so check if we were
|
||||
// initialized
|
||||
// // now before applying the state.
|
||||
// if (state != null && (!isInitialized || force)) {
|
||||
// index = state.index
|
||||
// parent = state.parent
|
||||
// _queue = state.queue.toMutableList()
|
||||
// repeatMode = state.repeatMode
|
||||
// isShuffled = state.isShuffled
|
||||
//
|
||||
// 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 {
|
||||
logD("Saving state to DB")
|
||||
|
||||
// Create the saved state from the current playback state.
|
||||
val state =
|
||||
synchronized(this) {
|
||||
PlaybackStateDatabase.SavedState(
|
||||
index = index,
|
||||
parent = parent,
|
||||
queue = _queue,
|
||||
positionMs = playerState.calculateElapsedPositionMs(),
|
||||
isShuffled = isShuffled,
|
||||
repeatMode = repeatMode)
|
||||
}
|
||||
return try {
|
||||
withContext(Dispatchers.IO) { database.write(state) }
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to save playback state.")
|
||||
logE(e.stackTraceToString())
|
||||
false
|
||||
}
|
||||
return false
|
||||
// // Create the saved state from the current playback state.
|
||||
// val state =
|
||||
// synchronized(this) {
|
||||
// PlaybackStateDatabase.SavedState(
|
||||
// index = index,
|
||||
// parent = parent,
|
||||
// queue = _queue,
|
||||
// positionMs = playerState.calculateElapsedPositionMs(),
|
||||
// isShuffled = isShuffled,
|
||||
// repeatMode = repeatMode)
|
||||
// }
|
||||
// return try {
|
||||
// withContext(Dispatchers.IO) { database.write(state) }
|
||||
// true
|
||||
// } catch (e: Exception) {
|
||||
// logE("Unable to save playback state.")
|
||||
// logE(e.stackTraceToString())
|
||||
// false
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -543,54 +478,57 @@ class PlaybackStateManager private constructor() {
|
|||
*/
|
||||
@Synchronized
|
||||
fun sanitize(newLibrary: MusicStore.Library) {
|
||||
if (!isInitialized) {
|
||||
// Nothing playing, nothing to do.
|
||||
logD("Not initialized, no need to sanitize")
|
||||
return
|
||||
}
|
||||
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
|
||||
logD("Sanitizing state")
|
||||
|
||||
// While we could just save and reload the state, we instead sanitize the state
|
||||
// at runtime for better performance (and to sidestep a co-routine on behalf of the caller).
|
||||
|
||||
// Sanitize parent
|
||||
parent =
|
||||
parent?.let {
|
||||
when (it) {
|
||||
is Album -> newLibrary.sanitize(it)
|
||||
is Artist -> newLibrary.sanitize(it)
|
||||
is Genre -> newLibrary.sanitize(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize queue. Make sure we re-align the index to point to the previously playing
|
||||
// Song in the queue queue.
|
||||
val oldSongUid = song?.uid
|
||||
_queue = _queue.mapNotNullTo(mutableListOf()) { newLibrary.sanitize(it) }
|
||||
while (song?.uid != oldSongUid && index > -1) {
|
||||
index--
|
||||
}
|
||||
|
||||
notifyNewPlayback()
|
||||
|
||||
val oldPosition = playerState.calculateElapsedPositionMs()
|
||||
// Continuing playback while also possibly doing drastic state updates is
|
||||
// a bad idea, so pause.
|
||||
internalPlayer.loadSong(song, false)
|
||||
if (index > -1) {
|
||||
// Internal player may have reloaded the media item, re-seek to the previous position
|
||||
seekTo(oldPosition)
|
||||
}
|
||||
// if (!isInitialized) {
|
||||
// // Nothing playing, nothing to do.
|
||||
// logD("Not initialized, no need to sanitize")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// val internalPlayer = internalPlayer ?: return
|
||||
//
|
||||
// logD("Sanitizing state")
|
||||
//
|
||||
// // While we could just save and reload the state, we instead sanitize the state
|
||||
// // at runtime for better performance (and to sidestep a co-routine on behalf of
|
||||
// the caller).
|
||||
//
|
||||
// // Sanitize parent
|
||||
// parent =
|
||||
// parent?.let {
|
||||
// when (it) {
|
||||
// is Album -> newLibrary.sanitize(it)
|
||||
// is Artist -> newLibrary.sanitize(it)
|
||||
// is Genre -> newLibrary.sanitize(it)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Sanitize queue. Make sure we re-align the index to point to the previously
|
||||
// playing
|
||||
// // Song in the queue queue.
|
||||
// val oldSongUid = song?.uid
|
||||
// _queue = _queue.mapNotNullTo(mutableListOf()) { newLibrary.sanitize(it) }
|
||||
// while (song?.uid != oldSongUid && index > -1) {
|
||||
// index--
|
||||
// }
|
||||
//
|
||||
// notifyNewPlayback()
|
||||
//
|
||||
// val oldPosition = playerState.calculateElapsedPositionMs()
|
||||
// // Continuing playback while also possibly doing drastic state updates is
|
||||
// // a bad idea, so pause.
|
||||
// internalPlayer.loadSong(song, false)
|
||||
// if (index > -1) {
|
||||
// // Internal player may have reloaded the media item, re-seek to the previous
|
||||
// position
|
||||
// seekTo(oldPosition)
|
||||
// }
|
||||
}
|
||||
|
||||
// --- CALLBACKS ---
|
||||
|
||||
private fun notifyIndexMoved() {
|
||||
for (callback in listeners) {
|
||||
callback.onIndexMoved(index)
|
||||
callback.onIndexMoved(queue)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -602,13 +540,13 @@ class PlaybackStateManager private constructor() {
|
|||
|
||||
private fun notifyQueueReworked() {
|
||||
for (callback in listeners) {
|
||||
callback.onQueueReworked(index, queue)
|
||||
callback.onQueueReworked(queue)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyNewPlayback() {
|
||||
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
|
||||
* [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
|
||||
* [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.
|
||||
* @param queue The new queue.
|
||||
* Called when the [Queue] changed in a trivial manner, such as a move.
|
||||
* @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
|
||||
* currently playing [Song] has not.
|
||||
* @param index The new position in the queue.
|
||||
* 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 onQueueReworked(index: Int, queue: List<Song>) {}
|
||||
fun onQueueReworked(queue: Queue) {}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
|
||||
fun onNewPlayback(queue: Queue, parent: MusicParent?) {}
|
||||
|
||||
/**
|
||||
* Called when the state of the [InternalPlayer] changes.
|
||||
|
@ -674,13 +605,6 @@ class PlaybackStateManager private constructor() {
|
|||
* @param repeatMode The new [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 {
|
||||
|
|
235
app/src/main/java/org/oxycblt/auxio/playback/state/Queue.kt
Normal file
235
app/src/main/java/org/oxycblt/auxio/playback/state/Queue.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
|||
class MediaButtonReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
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.
|
||||
// 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
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.playback.ActionMode
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
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.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -92,22 +93,29 @@ class MediaSessionComponent(private val context: Context, private val listener:
|
|||
|
||||
// --- PLAYBACKSTATEMANAGER OVERRIDES ---
|
||||
|
||||
override fun onIndexMoved(index: Int) {
|
||||
updateMediaMetadata(playbackManager.song, playbackManager.parent)
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
updateMediaMetadata(playbackManager.queue.currentSong, playbackManager.parent)
|
||||
invalidateSessionState()
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: List<Song>) {
|
||||
override fun onQueueChanged(queue: Queue) {
|
||||
updateQueue(queue)
|
||||
}
|
||||
|
||||
override fun onQueueReworked(index: Int, queue: List<Song>) {
|
||||
override fun onQueueReworked(queue: Queue) {
|
||||
updateQueue(queue)
|
||||
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?) {
|
||||
updateMediaMetadata(playbackManager.song, parent)
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||
updateMediaMetadata(playbackManager.queue.currentSong, parent)
|
||||
updateQueue(queue)
|
||||
invalidateSessionState()
|
||||
}
|
||||
|
@ -131,23 +139,12 @@ class MediaSessionComponent(private val context: Context, private val listener:
|
|||
invalidateSecondaryAction()
|
||||
}
|
||||
|
||||
override fun onShuffledChanged(isShuffled: Boolean) {
|
||||
mediaSession.setShuffleMode(
|
||||
if (isShuffled) {
|
||||
PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||
} else {
|
||||
PlaybackStateCompat.SHUFFLE_MODE_NONE
|
||||
})
|
||||
|
||||
invalidateSecondaryAction()
|
||||
}
|
||||
|
||||
// --- SETTINGS OVERRIDES ---
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -219,16 +216,13 @@ class MediaSessionComponent(private val context: Context, private val listener:
|
|||
}
|
||||
|
||||
override fun onSetShuffleMode(shuffleMode: Int) {
|
||||
playbackManager.reshuffle(
|
||||
playbackManager.reorder(
|
||||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
|
||||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
|
||||
settings)
|
||||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP)
|
||||
}
|
||||
|
||||
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?) {
|
||||
|
@ -318,9 +312,9 @@ class MediaSessionComponent(private val context: Context, private val listener:
|
|||
* Upload a new queue to the [MediaSessionCompat].
|
||||
* @param queue The current queue to upload.
|
||||
*/
|
||||
private fun updateQueue(queue: List<Song>) {
|
||||
private fun updateQueue(queue: Queue) {
|
||||
val queueItems =
|
||||
queue.mapIndexed { i, song ->
|
||||
queue.resolve().mapIndexed { i, song ->
|
||||
val description =
|
||||
MediaDescriptionCompat.Builder()
|
||||
// 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())
|
||||
.setActions(ACTIONS)
|
||||
// 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.
|
||||
|
||||
|
@ -361,7 +355,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
|
|||
PlaybackStateCompat.CustomAction.Builder(
|
||||
PlaybackService.ACTION_INVERT_SHUFFLE,
|
||||
context.getString(R.string.desc_shuffle),
|
||||
if (playbackManager.isShuffled) {
|
||||
if (playbackManager.queue.isShuffled) {
|
||||
R.drawable.ic_shuffle_on_24
|
||||
} else {
|
||||
R.drawable.ic_shuffle_off_24
|
||||
|
@ -391,7 +385,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
|
|||
invalidateSessionState()
|
||||
|
||||
when (settings.playbackNotificationAction) {
|
||||
ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.isShuffled)
|
||||
ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.queue.isShuffled)
|
||||
else -> notification.updateRepeatMode(playbackManager.repeatMode)
|
||||
}
|
||||
|
||||
|
|
|
@ -351,12 +351,16 @@ class PlaybackService :
|
|||
}
|
||||
// Shuffle all -> Start new playback from all songs
|
||||
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
|
||||
is InternalPlayer.Action.Open -> {
|
||||
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)
|
||||
ACTION_INC_REPEAT_MODE ->
|
||||
playbackManager.repeatMode = playbackManager.repeatMode.increment()
|
||||
ACTION_INVERT_SHUFFLE ->
|
||||
playbackManager.reshuffle(!playbackManager.isShuffled, settings)
|
||||
ACTION_INVERT_SHUFFLE -> playbackManager.reorder(!playbackManager.queue.isShuffled)
|
||||
ACTION_SKIP_PREV -> playbackManager.prev()
|
||||
ACTION_SKIP_NEXT -> playbackManager.next()
|
||||
ACTION_EXIT -> {
|
||||
|
@ -428,7 +431,7 @@ class PlaybackService :
|
|||
// which would result in unexpected playback. Work around it by dropping the first
|
||||
// call to this function, which should come from that Intent.
|
||||
if (settings.headsetAutoplay &&
|
||||
playbackManager.song != null &&
|
||||
playbackManager.queue.currentSong != null &&
|
||||
initialHeadsetPlugEventHandled) {
|
||||
logD("Device connected, resuming")
|
||||
playbackManager.setPlaying(true)
|
||||
|
@ -436,7 +439,7 @@ class PlaybackService :
|
|||
}
|
||||
|
||||
private fun pauseFromHeadsetPlug() {
|
||||
if (playbackManager.song != null) {
|
||||
if (playbackManager.queue.currentSong != null) {
|
||||
logD("Device disconnected, pausing")
|
||||
playbackManager.setPlaying(false)
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
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.settings.Settings
|
||||
import org.oxycblt.auxio.util.getDimenPixels
|
||||
|
@ -55,7 +56,7 @@ class WidgetComponent(private val context: Context) :
|
|||
|
||||
/** Update [WidgetProvider] with the current playback state. */
|
||||
fun update() {
|
||||
val song = playbackManager.song
|
||||
val song = playbackManager.queue.currentSong
|
||||
if (song == null) {
|
||||
logD("No song, resetting widget")
|
||||
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.
|
||||
val isPlaying = playbackManager.playerState.isPlaying
|
||||
val repeatMode = playbackManager.repeatMode
|
||||
val isShuffled = playbackManager.isShuffled
|
||||
val isShuffled = playbackManager.queue.isShuffled
|
||||
|
||||
provider.load(
|
||||
song,
|
||||
|
@ -115,10 +116,10 @@ class WidgetComponent(private val context: Context) :
|
|||
|
||||
// Hook all the major song-changing updates + the major player state updates
|
||||
// to updating the "Now Playing" widget.
|
||||
override fun onIndexMoved(index: Int) = update()
|
||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) = update()
|
||||
override fun onIndexMoved(queue: Queue) = update()
|
||||
override fun onQueueReworked(queue: Queue) = update()
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) = update()
|
||||
override fun onStateChanged(state: InternalPlayer.State) = update()
|
||||
override fun onShuffledChanged(isShuffled: Boolean) = update()
|
||||
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
|
|
Loading…
Reference in a new issue