From 1d63ad5b7b1c19b9dfddb883facaec9cf778d097 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 9 Jan 2024 15:04:32 -0700 Subject: [PATCH] playback: mirror state internally Mirror the last playback state of the holder inside PlaybackStateManager. This is generally more efficient and will enable better handling of when state holders attach and detach. --- CHANGELOG.md | 1 + .../auxio/playback/PlaybackViewModel.kt | 96 ++-- .../org/oxycblt/auxio/playback/queue/Queue.kt | 449 ------------------ .../auxio/playback/queue/QueueViewModel.kt | 78 +-- .../replaygain/ReplayGainAudioProcessor.kt | 44 +- .../playback/state/PlaybackStateHolder.kt | 31 +- .../playback/state/PlaybackStateManager.kt | 215 +++++++-- .../auxio/playback/system/ExoPlayerExt.kt | 66 ++- .../playback/system/MediaSessionComponent.kt | 119 ++--- .../auxio/playback/system/PlaybackService.kt | 108 +---- .../oxycblt/auxio/widgets/WidgetComponent.kt | 31 +- 11 files changed, 436 insertions(+), 802 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a01e2ec..91100cae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ #### What's Fixed - Fixed a crash occuring if you navigated to the settings page from the playlist view and then back +- Fixed music loading failing with an SQL error with certain music folder configurations ## 3.3.0 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index b12287bfc..73f7d1258 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -37,8 +37,8 @@ import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.state.DeferredPlayback -import org.oxycblt.auxio.playback.state.PlaybackEvent import org.oxycblt.auxio.playback.state.PlaybackStateManager +import org.oxycblt.auxio.playback.state.Progression import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.util.Event @@ -130,52 +130,56 @@ constructor( playbackSettings.unregisterListener(this) } - override fun onPlaybackEvent(event: PlaybackEvent) { - when (event) { - is PlaybackEvent.IndexMoved -> { - logD("Index moved, updating current song") - _song.value = event.currentSong - } - is PlaybackEvent.QueueChanged -> { - // Other types of queue changes preserve the current song. - if (event.change.type == QueueChange.Type.SONG) { - logD("Queue changed, updating current song") - _song.value = event.currentSong + override fun onIndexMoved(index: Int) { + logD("Index moved, updating current song") + _song.value = playbackManager.currentSong + } + + override fun onQueueChanged(queue: List, index: Int, change: QueueChange) { + // Other types of queue changes preserve the current song. + if (change.type == QueueChange.Type.SONG) { + logD("Queue changed, updating current song") + _song.value = playbackManager.currentSong + } + } + + override fun onQueueReordered(queue: List, index: Int, isShuffled: Boolean) { + logD("Queue completely changed, updating current song") + _isShuffled.value = isShuffled + } + + override fun onNewPlayback( + parent: MusicParent?, + queue: List, + index: Int, + isShuffled: Boolean + ) { + logD("New playback started, updating playback information") + _song.value = playbackManager.currentSong + _parent.value = parent + _isShuffled.value = isShuffled + } + + override fun onProgressionChanged(progression: Progression) { + logD("Player state changed, starting new position polling") + _isPlaying.value = progression.isPlaying + // Still need to update the position now due to co-routine launch delays + _positionDs.value = progression.calculateElapsedPositionMs().msToDs() + // Replace the previous position co-routine with a new one that uses the new + // state information. + lastPositionJob?.cancel() + lastPositionJob = + viewModelScope.launch { + while (true) { + _positionDs.value = progression.calculateElapsedPositionMs().msToDs() + // Wait a deci-second for the next position tick. + delay(100) } } - is PlaybackEvent.QueueReordered -> { - logD("Queue completely changed, updating current song") - _isShuffled.value = event.isShuffled - } - is PlaybackEvent.NewPlayback -> { - logD("New playback started, updating playback information") - _song.value = event.currentSong - _parent.value = event.parent - _isShuffled.value = event.isShuffled - } - is PlaybackEvent.ProgressionChanged -> { - logD("Progression changed, starting new position polling") - _isPlaying.value = event.progression.isPlaying - // Still need to update the position now due to co-routine launch delays - _positionDs.value = event.progression.calculateElapsedPositionMs().msToDs() - // Replace the previous position co-routine with a new one that uses the new - // state information. - lastPositionJob?.cancel() - lastPositionJob = - viewModelScope.launch { - while (true) { - _positionDs.value = - event.progression.calculateElapsedPositionMs().msToDs() - // Wait a deci-second for the next position tick. - delay(100) - } - } - } - is PlaybackEvent.RepeatModeChanged -> { - logD("Repeat mode changed, updating current mode") - _repeatMode.value = event.repeatMode - } - } + } + + override fun onRepeatModeChanged(repeatMode: RepeatMode) { + _repeatMode.value = repeatMode } override fun onBarActionChanged() { @@ -579,7 +583,7 @@ constructor( /** Toggle [isShuffled] (ex. from on to off) */ fun toggleShuffled() { logD("Toggling shuffled state") - playbackManager.reorder(!playbackManager.isShuffled) + playbackManager.shuffled(!playbackManager.isShuffled) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt deleted file mode 100644 index bc421b846..000000000 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * Queue.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.playback.queue - -import kotlin.random.Random -import kotlin.random.nextInt -import org.oxycblt.auxio.list.adapter.UpdateInstructions -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.util.logD - -/** - * A heap-backed play queue. - * - * Whereas other queue implementations use a plain list, Auxio requires a more complicated data - * structure in order to implement features such as gapless playback in ExoPlayer. This queue - * implementation is instead based around an unorganized "heap" of [Song] instances, that are then - * interpreted into different queues depending on the current playback configuration. - * - * In general, the implementation details don't need to be known for this data structure to be used, - * except in special circumstances like [SavedState]. The functions exposed should be familiar for - * any typical play queue. - * - * @author OxygenCobalt - */ -interface Queue { - /** The index of the currently playing [Song] in the current mapping. */ - val index: Int - /** The currently playing [Song]. */ - val currentSong: Song? - /** Whether this queue is shuffled. */ - val isShuffled: Boolean - /** - * Resolve this queue into a more conventional list of [Song]s. - * - * @return A list of [Song] corresponding to the current queue mapping. - */ - fun resolve(): List - - /** - * Represents the possible changes that can occur during certain queue mutation events. - * - * @param type The [Type] of the change to the internal queue state. - * @param instructions The update done to the resolved queue list. - */ - data class Change(val type: Type, val instructions: UpdateInstructions) { - enum class Type { - /** Only the mapping has changed. */ - MAPPING, - - /** The mapping has changed, and the index also changed to align with it. */ - INDEX, - - /** - * The current song has changed, possibly alongside the mapping and index depending on - * the context. - */ - SONG - } - } - - /** - * An immutable representation of the queue state. - * - * @param heap The heap of [Song]s that are/were used in the queue. This can be modified with - * null values to represent [Song]s that were "lost" from the heap without having to change - * other values. - * @param orderedMapping The mapping of the [heap] to an ordered queue. - * @param shuffledMapping The mapping of the [heap] to a shuffled queue. - * @param index The index of the currently playing [Song] at the time of serialization. - * @param songUid The [Music.UID] of the [Song] that was originally at [index]. - */ - class SavedState( - val heap: List, - val orderedMapping: List, - val shuffledMapping: List, - val index: Int, - val songUid: Music.UID, - ) { - /** - * Remaps the [heap] of this instance based on the given mapping function and copies it into - * a new [SavedState]. - * - * @param transform Code to remap the existing [Song] heap into a new [Song] heap. This - * **MUST** be the same size as the original heap. [Song] instances that could not be - * converted should be replaced with null in the new heap. - * @throws IllegalStateException If the invariant specified by [transform] is violated. - */ - inline fun remap(transform: (Song?) -> Song?) = - SavedState(heap.map(transform), orderedMapping, shuffledMapping, index, songUid) - } -} - -class MutableQueue : Queue { - @Volatile private var heap = mutableListOf() - @Volatile private var orderedMapping = mutableListOf() - @Volatile private var shuffledMapping = mutableListOf() - @Volatile - override var index = -1 - private set - - override val currentSong: Song? - get() = - shuffledMapping - .ifEmpty { orderedMapping.ifEmpty { null } } - ?.getOrNull(index) - ?.let(heap::get) - - override val isShuffled: Boolean - get() = shuffledMapping.isNotEmpty() - - override fun resolve() = - if (currentSong != null) { - shuffledMapping.map { heap[it] }.ifEmpty { orderedMapping.map { heap[it] } } - } else { - // Queue doesn't exist, return saner data. - listOf() - } - - /** - * Go to a particular index in the queue. - * - * @param to The index of the [Song] to start playing, in the current queue mapping. - * @return true if the queue jumped to that position, false otherwise. - */ - fun goto(to: Int): Boolean { - if (to !in orderedMapping.indices) { - return false - } - index = to - return true - } - - /** - * Start a new queue configuration. - * - * @param play The [Song] to play, or null to start from a random position. - * @param queue The queue of [Song]s to play. Must contain [play]. This list will become the - * heap internally. - * @param shuffled Whether to shuffle the queue or not. This changes the interpretation of - * [queue]. - */ - fun start(play: Song?, queue: List, shuffled: Boolean) { - heap = queue.toMutableList() - orderedMapping = MutableList(queue.size) { it } - shuffledMapping = mutableListOf() - index = - play?.let(queue::indexOf) ?: if (shuffled) Random.Default.nextInt(queue.indices) else 0 - reorder(shuffled) - check() - } - - /** - * Re-order the queue. - * - * @param shuffled Whether the queue should be shuffled or not. - */ - fun reorder(shuffled: Boolean) { - if (orderedMapping.isEmpty()) { - // Nothing to do. - return - } - - logD("Reordering queue [shuffled=$shuffled]") - - if (shuffled) { - val trueIndex = - if (shuffledMapping.isNotEmpty()) { - // Re-shuffling, song to preserve is in the shuffled mapping - shuffledMapping[index] - } else { - // First shuffle, song to preserve is in the ordered mapping - orderedMapping[index] - } - - // Since we are re-shuffling existing songs, we use the previous mapping size - // instead of the total queue size. - shuffledMapping = orderedMapping.shuffled().toMutableList() - shuffledMapping.add(0, shuffledMapping.removeAt(shuffledMapping.indexOf(trueIndex))) - index = 0 - } else if (shuffledMapping.isNotEmpty()) { - // Ordering queue, song to preserve is in the shuffled mapping. - index = orderedMapping.indexOf(shuffledMapping[index]) - shuffledMapping = mutableListOf() - } - check() - } - - /** - * Add [Song]s to the "top" of the queue (right next to the currently playing song). Will start - * playback if nothing is playing. - * - * @param songs The [Song]s to add. - * @return A [Queue.Change] instance that reflects the changes made. - */ - fun addToTop(songs: List): Queue.Change { - logD("Adding ${songs.size} songs to the front of the queue") - val insertAt = index + 1 - val heapIndices = songs.map(::addSongToHeap) - if (shuffledMapping.isNotEmpty()) { - // Add the new songs in front of the current index in the shuffled mapping and in front - // of the analogous list song in the ordered mapping. - logD("Must append songs to shuffled mapping") - val orderedIndex = orderedMapping.indexOf(shuffledMapping[index]) - orderedMapping.addAll(orderedIndex + 1, heapIndices) - shuffledMapping.addAll(insertAt, heapIndices) - } else { - // Add the new song in front of the current index in the ordered mapping. - logD("Only appending songs to ordered mapping") - orderedMapping.addAll(insertAt, heapIndices) - } - check() - return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size)) - } - - /** - * Add [Song]s to the end of the queue. Will start playback if nothing is playing. - * - * @param songs The [Song]s to add. - * @return A [Queue.Change] instance that reflects the changes made. - */ - fun addToBottom(songs: List): Queue.Change { - logD("Adding ${songs.size} songs to the back of the queue") - val insertAt = orderedMapping.size - val heapIndices = songs.map(::addSongToHeap) - // Can simple append the new songs to the end of both mappings. - orderedMapping.addAll(heapIndices) - if (shuffledMapping.isNotEmpty()) { - logD("Appending songs to shuffled mapping") - shuffledMapping.addAll(heapIndices) - } - check() - return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size)) - } - - /** - * Move a [Song] at the given position to a new position. - * - * @param src The position of the [Song] to move. - * @param dst The destination position of the [Song]. - * @return A [Queue.Change] instance that reflects the changes made. - */ - fun move(src: Int, dst: Int): Queue.Change { - if (shuffledMapping.isNotEmpty()) { - // Move songs only in the shuffled mapping. There is no sane analogous form of - // this for the ordered mapping. - shuffledMapping.add(dst, shuffledMapping.removeAt(src)) - } else { - // Move songs in the ordered mapping. - orderedMapping.add(dst, orderedMapping.removeAt(src)) - } - - val oldIndex = index - when (index) { - // We are moving the currently playing song, correct the index to it's new position. - src -> { - logD("Moving current song, shifting index") - index = dst - } - // We have moved an song from behind the playing song to in front, shift back. - in (src + 1)..dst -> { - logD("Moving song from behind -> front, shift backwards") - index -= 1 - } - // We have moved an song from in front of the playing song to behind, shift forward. - in dst until src -> { - logD("Moving song from front -> behind, shift forward") - index += 1 - } - else -> { - // Nothing to do. - logD("Move preserved index") - check() - return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Move(src, dst)) - } - } - - logD("Move changed index: $oldIndex -> $index") - - check() - return Queue.Change(Queue.Change.Type.INDEX, UpdateInstructions.Move(src, dst)) - } - - /** - * Remove a [Song] at the given position. - * - * @param at The position of the [Song] to remove. - * @return A [Queue.Change] instance that reflects the changes made. - */ - fun remove(at: Int): Queue.Change { - val lastIndex = orderedMapping.lastIndex - if (shuffledMapping.isNotEmpty()) { - // Remove the specified index in the shuffled mapping and the analogous song in the - // ordered mapping. - orderedMapping.removeAt(orderedMapping.indexOf(shuffledMapping[at])) - shuffledMapping.removeAt(at) - } else { - // Remove the specified index in the shuffled mapping - orderedMapping.removeAt(at) - } - - // Note: We do not clear songs out from the heap, as that would require the backing data - // of the player to be completely invalidated. It's generally easier to not remove the - // song and retain player state consistency. - - val type = - when { - // We just removed the currently playing song. - index == at -> { - logD("Removed current song") - if (lastIndex == index) { - logD("Current song at end of queue, shift back") - --index - } - Queue.Change.Type.SONG - } - // Index was ahead of removed song, shift back to preserve consistency. - index > at -> { - logD("Removed before current song, shift back") - --index - Queue.Change.Type.INDEX - } - // Nothing to do - else -> { - logD("Removal preserved index") - Queue.Change.Type.MAPPING - } - } - logD("Committing change of type $type") - check() - return Queue.Change(type, UpdateInstructions.Remove(at, 1)) - } - - /** - * Convert the current state of this instance into a [Queue.SavedState]. - * - * @return A new [Queue.SavedState] reflecting the exact state of the queue when called. - */ - fun toSavedState() = - currentSong?.let { song -> - Queue.SavedState( - heap.toList(), orderedMapping.toList(), shuffledMapping.toList(), index, song.uid) - } - - /** - * Update this instance from the given [Queue.SavedState]. - * - * @param savedState A [Queue.SavedState] with a valid queue representation. - */ - fun applySavedState(savedState: Queue.SavedState) { - val adjustments = mutableListOf() - var currentShift = 0 - for (song in savedState.heap) { - if (song != null) { - adjustments.add(currentShift) - } else { - adjustments.add(null) - currentShift -= 1 - } - } - - logD("Created adjustment mapping [max shift=$currentShift]") - - heap = savedState.heap.filterNotNull().toMutableList() - orderedMapping = - savedState.orderedMapping.mapNotNullTo(mutableListOf()) { heapIndex -> - adjustments[heapIndex]?.let { heapIndex + it } - } - shuffledMapping = - savedState.shuffledMapping.mapNotNullTo(mutableListOf()) { heapIndex -> - adjustments[heapIndex]?.let { heapIndex + it } - } - - // Make sure we re-align the index to point to the previously playing song. - index = savedState.index - while (currentSong?.uid != savedState.songUid && index > -1) { - index-- - } - logD("Corrected index: ${savedState.index} -> $index") - check() - } - - private fun addSongToHeap(song: Song): Int { - // We want to first try to see if there are any "orphaned" songs in the queue - // that we can re-use. This way, we can reduce the memory used up by songs that - // were previously removed from the queue. - val currentMapping = orderedMapping - if (orderedMapping.isNotEmpty()) { - // While we could iterate through the queue and then check the mapping, it's - // faster if we first check the queue for all instances of this song, and then - // do a exclusion of this set of indices with the current mapping in order to - // obtain the orphaned songs. - val orphanCandidates = mutableSetOf() - for (entry in heap.withIndex()) { - if (entry.value == song) { - orphanCandidates.add(entry.index) - } - } - logD("Found orphans: ${orphanCandidates.map { heap[it] }}") - orphanCandidates.removeAll(currentMapping.toSet()) - if (orphanCandidates.isNotEmpty()) { - val orphan = orphanCandidates.first() - logD("Found an orphan that could be re-used: ${heap[orphan]}") - // There are orphaned songs, return the first one we find. - return orphan - } - } - // Nothing to re-use, add this song to the queue - logD("No orphan could be re-used") - heap.add(song) - return heap.lastIndex - } - - private fun check() { - check(!(heap.isEmpty() && (orderedMapping.isNotEmpty() || shuffledMapping.isNotEmpty()))) { - "Queue inconsistency detected: Empty heap with non-empty mappings" + - "[ordered: ${orderedMapping.size}, shuffled: ${shuffledMapping.size}]" - } - - check(shuffledMapping.isEmpty() || orderedMapping.size == shuffledMapping.size) { - "Queue inconsistency detected: Ordered mapping size ${orderedMapping.size} " + - "!= Shuffled mapping size ${shuffledMapping.size}" - } - - check(orderedMapping.all { it in heap.indices }) { - "Queue inconsistency detected: Ordered mapping indices out of heap bounds" - } - - check(shuffledMapping.all { it in heap.indices }) { - "Queue inconsistency detected: Shuffled mapping indices out of heap bounds" - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 8ec0b3cd8..1283fc909 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -24,8 +24,8 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.list.adapter.UpdateInstructions +import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.playback.state.PlaybackEvent import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.util.Event @@ -52,7 +52,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt val scrollTo: Event get() = _scrollTo - private val _index = MutableStateFlow(playbackManager.resolveQueue().index) + private val _index = MutableStateFlow(playbackManager.index) /** The index of the currently playing song in the queue. */ val index: StateFlow get() = _index @@ -61,45 +61,47 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt playbackManager.addListener(this) } - override fun onPlaybackEvent(event: PlaybackEvent) { - when (event) { - is PlaybackEvent.IndexMoved -> { - logD("Index moved, synchronizing and scrolling to new position") - _scrollTo.put(event.index) - _index.value = event.index - } - is PlaybackEvent.QueueChanged -> { - // Queue changed trivially due to item mo -> Diff queue, stay at current index. - logD("Updating queue display") - _queueInstructions.put(event.change.instructions) - _queue.value = event.queue.queue - if (event.change.type != QueueChange.Type.MAPPING) { - // Index changed, make sure it remains updated without actually scrolling to it. - logD("Index changed with queue, synchronizing new position") - _index.value = event.queue.index - } - } - is PlaybackEvent.QueueReordered -> { - // Queue changed completely -> Replace queue, update index - logD("Queue changed completely, replacing queue and position") - _queueInstructions.put(UpdateInstructions.Replace(0)) - _scrollTo.put(event.queue.index) - _queue.value = event.queue.queue - _index.value = event.queue.index - } - is PlaybackEvent.NewPlayback -> { - // Entirely new queue -> Replace queue, update index - logD("New playback, replacing queue and position") - _queueInstructions.put(UpdateInstructions.Replace(0)) - _scrollTo.put(event.queue.index) - _queue.value = event.queue.queue - _index.value = event.queue.index - } - is PlaybackEvent.RepeatModeChanged, - is PlaybackEvent.ProgressionChanged -> {} + override fun onIndexMoved(index: Int) { + logD("Index moved, synchronizing and scrolling to new position") + _scrollTo.put(index) + _index.value = index + } + + override fun onQueueChanged(queue: List, index: Int, change: QueueChange) { + // Queue changed trivially due to item mo -> Diff queue, stay at current index. + logD("Updating queue display") + _queueInstructions.put(change.instructions) + _queue.value = queue + if (change.type != QueueChange.Type.MAPPING) { + // Index changed, make sure it remains updated without actually scrolling to it. + logD("Index changed with queue, synchronizing new position") + _index.value = index } } + override fun onQueueReordered(queue: List, index: Int, isShuffled: Boolean) { + // Queue changed completely -> Replace queue, update index + logD("Queue changed completely, replacing queue and position") + _queueInstructions.put(UpdateInstructions.Replace(0)) + _scrollTo.put(index) + _queue.value = queue + _index.value = index + } + + override fun onNewPlayback( + parent: MusicParent?, + queue: List, + index: Int, + isShuffled: Boolean + ) { + // Entirely new queue -> Replace queue, update index + logD("New playback, replacing queue and position") + _queueInstructions.put(UpdateInstructions.Replace(0)) + _scrollTo.put(index) + _queue.value = queue + _index.value = index + } + override fun onCleared() { super.onCleared() playbackManager.removeListener(this) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index ff3c27d78..c94f32ae5 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -27,9 +27,9 @@ import java.nio.ByteBuffer import javax.inject.Inject import kotlin.math.pow import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackSettings -import org.oxycblt.auxio.playback.state.PlaybackEvent import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.util.logD @@ -70,32 +70,28 @@ constructor( // --- OVERRIDES --- - override fun onPlaybackEvent(event: PlaybackEvent) { - when (event) { - is PlaybackEvent.IndexMoved -> { - logD("Index moved, updating current song") - applyReplayGain(event.currentSong) - } - is PlaybackEvent.QueueChanged -> { - // Queue changed trivially due to item mo -> Diff queue, stay at current index. - logD("Updating queue display") - // Other types of queue changes preserve the current song. - if (event.change.type == QueueChange.Type.SONG) { - applyReplayGain(event.currentSong) - } - } - is PlaybackEvent.NewPlayback -> { - logD("New playback started, updating playback information") - applyReplayGain(event.currentSong) - } - is PlaybackEvent.ProgressionChanged, - is PlaybackEvent.QueueReordered, - is PlaybackEvent.RepeatModeChanged -> { - // Nothing to do - } + override fun onIndexMoved(index: Int) { + logD("Index moved, updating current song") + applyReplayGain(playbackManager.currentSong) + } + + override fun onQueueChanged(queue: List, index: Int, change: QueueChange) { + // Other types of queue changes preserve the current song. + if (change.type == QueueChange.Type.SONG) { + applyReplayGain(playbackManager.currentSong) } } + override fun onNewPlayback( + parent: MusicParent?, + queue: List, + index: Int, + isShuffled: Boolean + ) { + logD("New playback started, updating playback information") + applyReplayGain(playbackManager.currentSong) + } + override fun onReplayGainSettingsChanged() { // ReplayGain config changed, we need to set it up again. applyReplayGain(playbackManager.currentSong) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt index 710934a6d..c3b2fbbab 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt @@ -27,19 +27,19 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song interface PlaybackStateHolder { - val currentSong: Song? + val progression: Progression val repeatMode: RepeatMode - val progression: Progression - - val audioSessionId: Int - val parent: MusicParent? + fun resolveQueue(): List + + fun resolveIndex(): Int + val isShuffled: Boolean - fun resolveQueue(): Queue + val audioSessionId: Int fun newPlayback( queue: List, @@ -74,6 +74,21 @@ interface PlaybackStateHolder { fun handleDeferred(action: DeferredPlayback): Boolean } +sealed interface StateEvent { + data object IndexMoved : StateEvent + + data class QueueChanged(val instructions: UpdateInstructions, val songChanged: Boolean) : + StateEvent + + data object QueueReordered : StateEvent + + data object NewPlayback : StateEvent + + data object ProgressionChanged : StateEvent + + data object RepeatModeChanged : StateEvent +} + /** * Represents the possible changes that can occur during certain queue mutation events. * @@ -114,9 +129,9 @@ sealed interface DeferredPlayback { data class Open(val uri: Uri) : DeferredPlayback } -data class Queue(val index: Int, val queue: List) { +data class Queue(val songs: List, val index: Int) { companion object { - fun nil() = Queue(-1, emptyList()) + fun nil() = Queue(emptyList(), -1) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index d33c935ba..e6356bba2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -46,17 +46,17 @@ interface PlaybackStateManager { /** The current [Progression] state. */ val progression: Progression - val currentSong: Song? - val repeatMode: RepeatMode - val audioSessionId: Int - val parent: MusicParent? - val isShuffled: Boolean + val currentSong: Song? - fun resolveQueue(): Queue + val queue: List + + val index: Int + + val isShuffled: Boolean /** The audio session ID of the internal player. Null if no internal player exists. */ val currentAudioSessionId: Int? @@ -178,9 +178,9 @@ interface PlaybackStateManager { * * @param shuffled Whether to shuffle the queue or not. */ - fun reorder(shuffled: Boolean) + fun shuffled(shuffled: Boolean) - fun dispatchEvent(stateHolder: PlaybackStateHolder, vararg events: PlaybackEvent) + fun dispatchEvent(stateHolder: PlaybackStateHolder, event: StateEvent) /** * Start a [DeferredPlayback] for the current [PlaybackStateHolder] to handle eventually. @@ -240,7 +240,54 @@ interface PlaybackStateManager { * [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener]. */ interface Listener { - fun onPlaybackEvent(event: PlaybackEvent) + /** + * Called when the position of the currently playing item has changed, changing the current + * [Song], but no other queue attribute has changed. + */ + fun onIndexMoved(index: Int) {} + + /** + * Called when the [Queue] changed in a manner outlined by the given [Queue.Change]. + * + * @param queue The new [Queue]. + * @param change The type of [Queue.Change] that occurred. + */ + fun onQueueChanged(queue: List, index: Int, change: QueueChange) {} + + /** + * Called when the [Queue] has changed in a non-trivial manner (such as re-shuffling), but + * the currently playing [Song] has not. + * + * @param queue The new [Queue]. + */ + fun onQueueReordered(queue: List, index: Int, isShuffled: Boolean) {} + + /** + * Called when a new playback configuration was created. + * + * @param queue The new [Queue]. + * @param parent The new [MusicParent] being played from, or null if playing from all songs. + */ + fun onNewPlayback( + parent: MusicParent?, + queue: List, + index: Int, + isShuffled: Boolean + ) {} + + /** + * Called when the state of the [InternalPlayer] changes. + * + * @param progression The new state of the [InternalPlayer]. + */ + fun onProgressionChanged(progression: Progression) {} + + /** + * Called when the [RepeatMode] changes. + * + * @param repeatMode The new [RepeatMode]. + */ + fun onRepeatModeChanged(repeatMode: RepeatMode) {} } /** @@ -259,52 +306,52 @@ interface PlaybackStateManager { ) } -sealed interface PlaybackEvent { - class IndexMoved(val currentSong: Song?, val index: Int) : PlaybackEvent - - class QueueChanged(val currentSong: Song?, val queue: Queue, val change: QueueChange) : - PlaybackEvent - - class QueueReordered(val queue: Queue, val isShuffled: Boolean) : PlaybackEvent - - class NewPlayback( - val currentSong: Song, - val queue: Queue, - val parent: MusicParent?, - val isShuffled: Boolean, - ) : PlaybackEvent - - class ProgressionChanged(val progression: Progression) : PlaybackEvent - - class RepeatModeChanged(val repeatMode: RepeatMode) : PlaybackEvent -} - class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { + private data class StateMirror( + val progression: Progression, + val repeatMode: RepeatMode, + val parent: MusicParent?, + val queue: List, + val index: Int, + val isShuffled: Boolean, + ) + private val listeners = mutableListOf() + + @Volatile + private var stateMirror = + StateMirror( + progression = Progression.nil(), + repeatMode = RepeatMode.NONE, + parent = null, + queue = emptyList(), + index = -1, + isShuffled = false, + ) @Volatile private var stateHolder: PlaybackStateHolder? = null @Volatile private var pendingDeferredPlayback: DeferredPlayback? = null @Volatile private var isInitialized = false - /** The current [Progression] state. */ - override val progression: Progression - get() = stateHolder?.progression ?: Progression.nil() + override val progression + get() = stateMirror.progression - override val currentSong: Song? - get() = stateHolder?.currentSong + override val repeatMode + get() = stateMirror.repeatMode - override val repeatMode: RepeatMode - get() = stateHolder?.repeatMode ?: RepeatMode.NONE + override val parent + get() = stateMirror.parent - override val audioSessionId: Int - get() = stateHolder?.audioSessionId ?: -1 + override val currentSong + get() = stateMirror.queue.getOrNull(stateMirror.index) - override val parent: MusicParent? - get() = stateHolder?.parent + override val queue + get() = stateMirror.queue - override val isShuffled: Boolean - get() = stateHolder?.isShuffled ?: false + override val index + get() = stateMirror.index - override fun resolveQueue() = stateHolder?.resolveQueue() ?: Queue.nil() + override val isShuffled + get() = stateMirror.isShuffled override val currentAudioSessionId: Int? get() = stateHolder?.audioSessionId @@ -313,6 +360,14 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { override fun addListener(listener: PlaybackStateManager.Listener) { logD("Adding $listener to listeners") listeners.add(listener) + + if (isInitialized) { + logD("Sending initial state to $listener") + listener.onNewPlayback( + stateMirror.parent, stateMirror.queue, stateMirror.index, stateMirror.isShuffled) + listener.onProgressionChanged(stateMirror.progression) + listener.onRepeatModeChanged(stateMirror.repeatMode) + } } @Synchronized @@ -418,7 +473,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { } @Synchronized - override fun reorder(shuffled: Boolean) { + override fun shuffled(shuffled: Boolean) { val stateHolder = stateHolder ?: return logD("Reordering queue [shuffled=$shuffled]") stateHolder.reorder(shuffled) @@ -470,15 +525,79 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { } @Synchronized - override fun dispatchEvent(stateHolder: PlaybackStateHolder, vararg events: PlaybackEvent) { + override fun dispatchEvent(stateHolder: PlaybackStateHolder, event: StateEvent) { if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) { logW("Given internal player did not match current internal player") return } - events.forEach { event -> - logD("Dispatching event $event") - listeners.forEach { it.onPlaybackEvent(event) } + when (event) { + is StateEvent.IndexMoved -> { + stateMirror = + stateMirror.copy( + index = stateHolder.resolveIndex(), + ) + listeners.forEach { it.onIndexMoved(stateMirror.index) } + } + is StateEvent.QueueChanged -> { + val instructions = event.instructions + val newIndex = stateHolder.resolveIndex() + val changeType = + when { + event.songChanged -> { + QueueChange.Type.SONG + } + stateMirror.index != newIndex -> QueueChange.Type.INDEX + else -> QueueChange.Type.MAPPING + } + stateMirror = stateMirror.copy(queue = stateHolder.resolveQueue(), index = newIndex) + val change = QueueChange(changeType, instructions) + listeners.forEach { + it.onQueueChanged(stateMirror.queue, stateMirror.index, change) + } + } + is StateEvent.QueueReordered -> { + stateMirror = + stateMirror.copy( + queue = stateHolder.resolveQueue(), + index = stateHolder.resolveIndex(), + isShuffled = stateHolder.isShuffled, + ) + listeners.forEach { + it.onQueueReordered( + stateMirror.queue, stateMirror.index, stateMirror.isShuffled) + } + } + is StateEvent.NewPlayback -> { + stateMirror = + stateMirror.copy( + parent = stateHolder.parent, + queue = stateHolder.resolveQueue(), + index = stateHolder.resolveIndex(), + isShuffled = stateHolder.isShuffled, + ) + listeners.forEach { + it.onNewPlayback( + stateMirror.parent, + stateMirror.queue, + stateMirror.index, + stateMirror.isShuffled) + } + } + is StateEvent.ProgressionChanged -> { + stateMirror = + stateMirror.copy( + progression = stateHolder.progression, + ) + listeners.forEach { it.onProgressionChanged(stateMirror.progression) } + } + is StateEvent.RepeatModeChanged -> { + stateMirror = + stateMirror.copy( + repeatMode = stateHolder.repeatMode, + ) + listeners.forEach { it.onRepeatModeChanged(stateMirror.repeatMode) } + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/ExoPlayerExt.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/ExoPlayerExt.kt index c47e45473..d22bf6376 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/ExoPlayerExt.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/ExoPlayerExt.kt @@ -29,6 +29,19 @@ import org.oxycblt.auxio.util.logD val ExoPlayer.song get() = currentMediaItem?.song +fun ExoPlayer.resolveIndex() = unscrambleQueueIndices().indexOf(currentMediaItemIndex) + +fun ExoPlayer.resolveQueue() = unscrambleQueueIndices().map { getMediaItemAt(it).song } + +val ExoPlayer.repeat: RepeatMode + get() = + when (repeatMode) { + Player.REPEAT_MODE_OFF -> RepeatMode.NONE + Player.REPEAT_MODE_ONE -> RepeatMode.TRACK + Player.REPEAT_MODE_ALL -> RepeatMode.ALL + else -> throw IllegalStateException("Unknown repeat mode: $repeatMode") + } + fun ExoPlayer.orderedQueue(queue: Collection, start: Song?) { clearMediaItems() shuffleModeEnabled = false @@ -60,25 +73,6 @@ fun ExoPlayer.shuffledQueue(queue: Collection, start: Song?) { seekTo(currentTimeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET) } -val ExoPlayer.currentIndex: Int - get() { - val queue = unscrambleQueue { index -> index } - if (queue.isEmpty()) { - return C.INDEX_UNSET - } - - return queue.indexOf(currentMediaItemIndex) - } - -val ExoPlayer.repeat: RepeatMode - get() = - when (repeatMode) { - Player.REPEAT_MODE_OFF -> RepeatMode.NONE - Player.REPEAT_MODE_ONE -> RepeatMode.TRACK - Player.REPEAT_MODE_ALL -> RepeatMode.ALL - else -> throw IllegalStateException("Unknown repeat mode: $repeatMode") - } - fun ExoPlayer.reorder(shuffled: Boolean) { logD("Reordering queue to $shuffled") shuffleModeEnabled = shuffled @@ -97,23 +91,23 @@ fun ExoPlayer.addToQueue(songs: List) { } fun ExoPlayer.goto(index: Int) { - val queue = unscrambleQueue { index -> index } - if (queue.isEmpty()) { + val indices = unscrambleQueueIndices() + if (indices.isEmpty()) { return } - val trueIndex = queue[index] + val trueIndex = indices[index] seekTo(trueIndex, C.TIME_UNSET) } fun ExoPlayer.move(from: Int, to: Int) { - val queue = unscrambleQueue { index -> index } - if (queue.isEmpty()) { + val indices = unscrambleQueueIndices() + if (indices.isEmpty()) { return } - val trueFrom = queue[from] - val trueTo = queue[to] + val trueFrom = indices[from] + val trueTo = indices[to] when { trueFrom > trueTo -> { @@ -128,29 +122,25 @@ fun ExoPlayer.move(from: Int, to: Int) { } fun ExoPlayer.remove(at: Int) { - val queue = unscrambleQueue { index -> index } - if (queue.isEmpty()) { + val indices = unscrambleQueueIndices() + if (indices.isEmpty()) { return } - val trueIndex = queue[at] + val trueIndex = indices[at] removeMediaItem(trueIndex) } -fun ExoPlayer.resolveQueue(): List { - return unscrambleQueue { index -> getMediaItemAt(index).song } -} - -inline fun ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List { +fun ExoPlayer.unscrambleQueueIndices(): List { val timeline = currentTimeline if (timeline.isEmpty()) { return emptyList() } - val queue = mutableListOf() + val queue = mutableListOf() // Add the active queue item. val currentMediaItemIndex = currentMediaItemIndex - queue.add(mapper(currentMediaItemIndex)) + queue.add(currentMediaItemIndex) // Fill queue alternating with next and/or previous queue items. var firstMediaItemIndex = currentMediaItemIndex @@ -164,7 +154,7 @@ inline fun ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List { timeline.getNextWindowIndex( lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled) if (lastMediaItemIndex != C.INDEX_UNSET) { - queue.add(mapper(lastMediaItemIndex)) + queue.add(lastMediaItemIndex) } } if (firstMediaItemIndex != C.INDEX_UNSET) { @@ -172,7 +162,7 @@ inline fun ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List { timeline.getPreviousWindowIndex( firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled) if (firstMediaItemIndex != C.INDEX_UNSET) { - queue.add(0, mapper(firstMediaItemIndex)) + queue.add(0, firstMediaItemIndex) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 57ce5d483..cbccf56ec 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -38,9 +38,8 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.ActionMode import org.oxycblt.auxio.playback.PlaybackSettings -import org.oxycblt.auxio.playback.state.PlaybackEvent import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.playback.state.Queue +import org.oxycblt.auxio.playback.state.Progression import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.util.logD @@ -118,60 +117,66 @@ constructor( // --- PLAYBACKSTATEMANAGER OVERRIDES --- - override fun onPlaybackEvent(event: PlaybackEvent) { - when (event) { - is PlaybackEvent.IndexMoved -> { - updateMediaMetadata(event.currentSong, playbackManager.parent) - invalidateSessionState() - } - is PlaybackEvent.QueueChanged -> { - updateQueue(event.queue) - when (event.change.type) { - // Nothing special to do with mapping changes. - QueueChange.Type.MAPPING -> {} - // Index changed, ensure playback state's index changes. - QueueChange.Type.INDEX -> invalidateSessionState() - // Song changed, ensure metadata changes. - QueueChange.Type.SONG -> - updateMediaMetadata(event.currentSong, playbackManager.parent) - } - } - is PlaybackEvent.QueueReordered -> { - updateQueue(event.queue) - invalidateSessionState() - mediaSession.setShuffleMode( - if (event.isShuffled) { - PlaybackStateCompat.SHUFFLE_MODE_ALL - } else { - PlaybackStateCompat.SHUFFLE_MODE_NONE - }) - invalidateSecondaryAction() - } - is PlaybackEvent.NewPlayback -> { - updateMediaMetadata(event.currentSong, event.parent) - updateQueue(event.queue) - invalidateSessionState() - } - is PlaybackEvent.ProgressionChanged -> { - invalidateSessionState() - notification.updatePlaying(playbackManager.progression.isPlaying) - if (!bitmapProvider.isBusy) { - listener?.onPostNotification(notification) - } - } - is PlaybackEvent.RepeatModeChanged -> { - mediaSession.setRepeatMode( - when (event.repeatMode) { - RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE - RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE - RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL - }) + override fun onIndexMoved(index: Int) { + updateMediaMetadata(playbackManager.currentSong, playbackManager.parent) + invalidateSessionState() + } - invalidateSecondaryAction() - } + override fun onQueueChanged(queue: List, index: Int, change: QueueChange) { + updateQueue(queue) + when (change.type) { + // Nothing special to do with mapping changes. + QueueChange.Type.MAPPING -> {} + // Index changed, ensure playback state's index changes. + QueueChange.Type.INDEX -> invalidateSessionState() + // Song changed, ensure metadata changes. + QueueChange.Type.SONG -> + updateMediaMetadata(playbackManager.currentSong, playbackManager.parent) } } + override fun onQueueReordered(queue: List, index: Int, isShuffled: Boolean) { + updateQueue(queue) + invalidateSessionState() + mediaSession.setShuffleMode( + if (isShuffled) { + PlaybackStateCompat.SHUFFLE_MODE_ALL + } else { + PlaybackStateCompat.SHUFFLE_MODE_NONE + }) + invalidateSecondaryAction() + } + + override fun onNewPlayback( + parent: MusicParent?, + queue: List, + index: Int, + isShuffled: Boolean + ) { + updateMediaMetadata(playbackManager.currentSong, parent) + updateQueue(queue) + invalidateSessionState() + } + + override fun onProgressionChanged(progression: Progression) { + invalidateSessionState() + notification.updatePlaying(playbackManager.progression.isPlaying) + if (!bitmapProvider.isBusy) { + listener?.onPostNotification(notification) + } + } + + override fun onRepeatModeChanged(repeatMode: RepeatMode) { + mediaSession.setRepeatMode( + when (repeatMode) { + RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE + RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE + RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL + }) + + invalidateSecondaryAction() + } + // --- SETTINGS OVERRIDES --- override fun onImageSettingsChanged() { @@ -251,7 +256,7 @@ constructor( } override fun onSetShuffleMode(shuffleMode: Int) { - playbackManager.reorder( + playbackManager.shuffled( shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP) } @@ -355,9 +360,9 @@ constructor( * * @param queue The current queue to upload. */ - private fun updateQueue(queue: Queue) { + private fun updateQueue(queue: List) { val queueItems = - queue.queue.mapIndexed { i, song -> + queue.mapIndexed { i, song -> val description = MediaDescriptionCompat.Builder() // Media ID should not be the item index but rather the UID, @@ -382,15 +387,13 @@ constructor( private fun invalidateSessionState() { logD("Updating media session playback state") - val queue = playbackManager.resolveQueue() - val state = // InternalPlayer.State handles position/state information. playbackManager.progression .intoPlaybackState(PlaybackStateCompat.Builder()) .setActions(ACTIONS) // Active queue ID corresponds to the indices we populated prior, use them here. - .setActiveQueueItemId(queue.index.toLong()) + .setActiveQueueItemId(playbackManager.index.toLong()) // Android 13+ relies on custom actions in the notification. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index a7849e699..6c973c167 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -56,13 +56,11 @@ import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.state.DeferredPlayback -import org.oxycblt.auxio.playback.state.PlaybackEvent import org.oxycblt.auxio.playback.state.PlaybackStateHolder import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.Progression -import org.oxycblt.auxio.playback.state.Queue -import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.RepeatMode +import org.oxycblt.auxio.playback.state.StateEvent import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE @@ -219,9 +217,6 @@ class PlaybackService : // --- PLAYBACKSTATEHOLDER OVERRIDES --- - override val currentSong - get() = player.song - override val repeatMode get() = player.repeat @@ -237,16 +232,17 @@ class PlaybackService : } ?: Progression.nil() - override val audioSessionId: Int - get() = player.audioSessionId - override var parent: MusicParent? = null override val isShuffled get() = player.shuffleModeEnabled - override fun resolveQueue(): Queue = - player.song?.let { Queue(player.currentIndex, player.resolveQueue()) } ?: Queue.nil() + override fun resolveIndex() = player.resolveIndex() + + override fun resolveQueue() = player.resolveQueue() + + override val audioSessionId: Int + get() = player.audioSessionId override fun newPlayback( queue: List, @@ -263,19 +259,12 @@ class PlaybackService : } player.prepare() player.playWhenReady = play - playbackManager.dispatchEvent( - this, - PlaybackEvent.NewPlayback( - requireNotNull(player.song) { - "Inconsistency detected: Player does not have song despite being populated" - }, - Queue(player.currentIndex, player.resolveQueue()), - parent, - isShuffled)) + playbackManager.dispatchEvent(this, StateEvent.NewPlayback) } override fun playing(playing: Boolean) { player.playWhenReady = playing + // Dispatched later once all of the changes have been accumulated } override fun repeatMode(repeatMode: RepeatMode) { @@ -285,6 +274,7 @@ class PlaybackService : RepeatMode.ALL -> Player.REPEAT_MODE_ALL RepeatMode.TRACK -> Player.REPEAT_MODE_ONE } + playbackManager.dispatchEvent(this, StateEvent.RepeatModeChanged) } override fun seekTo(positionMs: Long) { @@ -293,100 +283,53 @@ class PlaybackService : override fun next() { player.seekToNext() + playbackManager.dispatchEvent(this, StateEvent.IndexMoved) player.play() } override fun prev() { player.seekToPrevious() + playbackManager.dispatchEvent(this, StateEvent.IndexMoved) player.play() } override fun goto(index: Int) { player.goto(index) + playbackManager.dispatchEvent(this, StateEvent.IndexMoved) player.play() } override fun reorder(shuffled: Boolean) { player.reorder(shuffled) - playbackManager.dispatchEvent( - this, - PlaybackEvent.QueueReordered( - Queue( - player.currentIndex, - player.resolveQueue(), - ), - shuffled)) + playbackManager.dispatchEvent(this, StateEvent.QueueReordered) } override fun addToQueue(songs: List) { - val insertAt = player.currentIndex + 1 + val insertAt = playbackManager.index + 1 player.addToQueue(songs) playbackManager.dispatchEvent( - this, - PlaybackEvent.QueueChanged( - requireNotNull(player.song) { - "Inconsistency detected: Player does not have song despite being populated" - }, - Queue(player.currentIndex, player.resolveQueue()), - QueueChange( - QueueChange.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size)))) + this, StateEvent.QueueChanged(UpdateInstructions.Add(insertAt, songs.size), false)) } override fun playNext(songs: List) { - val insertAt = player.currentIndex + 1 + val insertAt = playbackManager.index + 1 player.playNext(songs) - // TODO: Re-add queue changes playbackManager.dispatchEvent( - this, - PlaybackEvent.QueueChanged( - requireNotNull(player.song) { - "Inconsistency detected: Player does not have song despite being populated" - }, - Queue(player.currentIndex, player.resolveQueue()), - QueueChange( - QueueChange.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size)))) + this, StateEvent.QueueChanged(UpdateInstructions.Add(insertAt, songs.size), false)) } override fun move(from: Int, to: Int) { - val oldIndex = player.currentIndex player.move(from, to) - val changeType = - if (player.currentIndex != oldIndex) { - QueueChange.Type.INDEX - } else { - QueueChange.Type.MAPPING - } playbackManager.dispatchEvent( - this, - PlaybackEvent.QueueChanged( - requireNotNull(player.song) { - "Inconsistency detected: Player does not have song despite being populated" - }, - Queue(player.currentIndex, player.resolveQueue()), - QueueChange(changeType, UpdateInstructions.Move(from, to)))) + this, StateEvent.QueueChanged(UpdateInstructions.Move(from, to), false)) } override fun remove(at: Int) { - val oldUnscrambledIndex = player.currentIndex - val oldScrambledIndex = player.currentMediaItemIndex + val oldIndex = player.currentMediaItemIndex player.remove(at) - val newUnscrambledIndex = player.currentIndex - val newScrambledIndex = player.currentMediaItemIndex - val changeType = - when { - oldScrambledIndex != newScrambledIndex -> QueueChange.Type.SONG - oldUnscrambledIndex != newUnscrambledIndex -> QueueChange.Type.INDEX - else -> QueueChange.Type.MAPPING - } - + val newIndex = player.currentMediaItemIndex playbackManager.dispatchEvent( - this, - PlaybackEvent.QueueChanged( - requireNotNull(player.song) { - "Inconsistency detected: Player does not have song despite being populated" - }, - Queue(player.currentIndex, player.resolveQueue()), - QueueChange(changeType, UpdateInstructions.Remove(at, 1)))) + this, StateEvent.QueueChanged(UpdateInstructions.Remove(at, 1), oldIndex != newIndex)) } // --- PLAYER OVERRIDES --- @@ -418,8 +361,7 @@ class PlaybackService : if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO || reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) { - playbackManager.dispatchEvent( - this, PlaybackEvent.IndexMoved(player.song, player.currentIndex)) + playbackManager.dispatchEvent(this, StateEvent.IndexMoved) } } @@ -431,7 +373,7 @@ class PlaybackService : Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_POSITION_DISCONTINUITY)) { logD("Player state changed, must synchronize state") - playbackManager.dispatchEvent(this, PlaybackEvent.ProgressionChanged(progression)) + playbackManager.dispatchEvent(this, StateEvent.ProgressionChanged) } } @@ -572,7 +514,7 @@ class PlaybackService : } ACTION_INVERT_SHUFFLE -> { logD("Received shuffle event") - playbackManager.reorder(!playbackManager.isShuffled) + playbackManager.shuffled(!playbackManager.isShuffled) } ACTION_SKIP_PREV -> { logD("Received skip previous event") diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index 2311ab53f..d67892300 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -29,10 +29,10 @@ import org.oxycblt.auxio.image.BitmapProvider import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.image.extractor.RoundedRectTransformation import org.oxycblt.auxio.image.extractor.SquareCropTransformation +import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.playback.queue.Queue -import org.oxycblt.auxio.playback.state.PlaybackEvent import org.oxycblt.auxio.playback.state.PlaybackStateManager +import org.oxycblt.auxio.playback.state.Progression import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.UISettings @@ -135,17 +135,28 @@ constructor( // --- CALLBACKS --- - override fun onPlaybackEvent(event: PlaybackEvent) { - if (event is PlaybackEvent.NewPlayback || - event is PlaybackEvent.ProgressionChanged || - (event is PlaybackEvent.QueueChanged && event.change.type == QueueChange.Type.SONG) || - event is PlaybackEvent.QueueReordered || - event is PlaybackEvent.IndexMoved || - event is PlaybackEvent.RepeatModeChanged) { + // Respond to all major song or player changes that will affect the widget + override fun onIndexMoved(index: Int) = update() + + override fun onQueueChanged(queue: List, index: Int, change: QueueChange) { + if (change.type == QueueChange.Type.SONG) { update() } } + override fun onQueueReordered(queue: List, index: Int, isShuffled: Boolean) = update() + + override fun onNewPlayback( + parent: MusicParent?, + queue: List, + index: Int, + isShuffled: Boolean + ) = update() + + override fun onProgressionChanged(progression: Progression) = update() + + override fun onRepeatModeChanged(repeatMode: RepeatMode) = update() + // Respond to settings changes that will affect the widget override fun onRoundModeChanged() = update() @@ -156,7 +167,7 @@ constructor( * * @param song [Queue.currentSong] * @param cover A pre-loaded album cover [Bitmap] for [song]. - * @param isPlaying [PlaybackStateManager.progression] + * @param isPlaying [PlaybackStateManager.playerState] * @param repeatMode [PlaybackStateManager.repeatMode] * @param isShuffled [Queue.isShuffled] */