playback: ramshack initial gapless playback impl
This commit is contained in:
parent
5d5356e46e
commit
26d14ec6e1
15 changed files with 1043 additions and 785 deletions
|
@ -31,7 +31,7 @@ import javax.inject.Inject
|
|||
import org.oxycblt.auxio.databinding.ActivityMainBinding
|
||||
import org.oxycblt.auxio.music.system.IndexerService
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
import org.oxycblt.auxio.playback.state.DeferredPlayback
|
||||
import org.oxycblt.auxio.playback.system.PlaybackService
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
import org.oxycblt.auxio.util.isNight
|
||||
|
@ -76,7 +76,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
if (!startIntentAction(intent)) {
|
||||
// No intent action to do, just restore the previously saved state.
|
||||
playbackModel.startAction(InternalPlayer.Action.RestoreState)
|
||||
playbackModel.playDeferred(DeferredPlayback.RestoreState)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,15 +137,15 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
val action =
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
|
||||
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
|
||||
Intent.ACTION_VIEW -> DeferredPlayback.Open(intent.data ?: return false)
|
||||
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> DeferredPlayback.ShuffleAll
|
||||
else -> {
|
||||
logW("Unexpected intent ${intent.action}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
logD("Translated intent to $action")
|
||||
playbackModel.startAction(action)
|
||||
playbackModel.playDeferred(action)
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,6 @@ import kotlinx.coroutines.Job
|
|||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.music.IndexingProgress
|
||||
import org.oxycblt.auxio.music.IndexingState
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.music.fs.contentResolverSafe
|
||||
|
@ -139,22 +138,23 @@ class IndexerService :
|
|||
logD("Music changed, updating shared objects")
|
||||
// Wipe possibly-invalidated outdated covers
|
||||
imageLoader.memoryCache?.clear()
|
||||
// Clear invalid models from PlaybackStateManager. This is not connected
|
||||
// to a listener as it is bad practice for a shared object to attach to
|
||||
// the listener system of another.
|
||||
playbackManager.toSavedState()?.let { savedState ->
|
||||
playbackManager.applySavedState(
|
||||
PlaybackStateManager.SavedState(
|
||||
parent =
|
||||
savedState.parent?.let { musicRepository.find(it.uid) as? MusicParent },
|
||||
queueState =
|
||||
savedState.queueState.remap { song ->
|
||||
deviceLibrary.findSong(requireNotNull(song).uid)
|
||||
},
|
||||
positionMs = savedState.positionMs,
|
||||
repeatMode = savedState.repeatMode),
|
||||
true)
|
||||
}
|
||||
// // Clear invalid models from PlaybackStateManager. This is not connected
|
||||
// // to a listener as it is bad practice for a shared object to attach to
|
||||
// // the listener system of another.
|
||||
// playbackManager.toSavedState()?.let { savedState ->
|
||||
// playbackManager.applySavedState(
|
||||
// PlaybackStateManager.SavedState(
|
||||
// parent =
|
||||
// savedState.parent?.let { musicRepository.find(it.uid) as?
|
||||
// MusicParent },
|
||||
// queueState =
|
||||
// savedState.queueState.remap { song ->
|
||||
// deviceLibrary.findSong(requireNotNull(song).uid)
|
||||
// },
|
||||
// positionMs = savedState.positionMs,
|
||||
// repeatMode = savedState.repeatMode),
|
||||
// true)
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onIndexingStateChanged() {
|
||||
|
|
|
@ -36,9 +36,10 @@ import org.oxycblt.auxio.music.MusicRepository
|
|||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||
import org.oxycblt.auxio.playback.queue.Queue
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
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.QueueChange
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.util.Event
|
||||
import org.oxycblt.auxio.util.MutableEvent
|
||||
|
@ -129,51 +130,52 @@ constructor(
|
|||
playbackSettings.unregisterListener(this)
|
||||
}
|
||||
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||
when (event) {
|
||||
is PlaybackEvent.IndexMoved -> {
|
||||
logD("Index moved, updating current song")
|
||||
_song.value = queue.currentSong
|
||||
_song.value = event.currentSong
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
|
||||
is PlaybackEvent.QueueChanged -> {
|
||||
// Other types of queue changes preserve the current song.
|
||||
if (change.type == Queue.Change.Type.SONG) {
|
||||
if (event.change.type == QueueChange.Type.SONG) {
|
||||
logD("Queue changed, updating current song")
|
||||
_song.value = queue.currentSong
|
||||
_song.value = event.currentSong
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQueueReordered(queue: Queue) {
|
||||
is PlaybackEvent.QueueReordered -> {
|
||||
logD("Queue completely changed, updating current song")
|
||||
_isShuffled.value = queue.isShuffled
|
||||
_isShuffled.value = event.isShuffled
|
||||
}
|
||||
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||
is PlaybackEvent.NewPlayback -> {
|
||||
logD("New playback started, updating playback information")
|
||||
_song.value = queue.currentSong
|
||||
_parent.value = parent
|
||||
_isShuffled.value = queue.isShuffled
|
||||
_song.value = event.currentSong
|
||||
_parent.value = event.parent
|
||||
_isShuffled.value = event.isShuffled
|
||||
}
|
||||
|
||||
override fun onStateChanged(state: InternalPlayer.State) {
|
||||
logD("Player state changed, starting new position polling")
|
||||
_isPlaying.value = state.isPlaying
|
||||
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 = state.calculateElapsedPositionMs().msToDs()
|
||||
_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 = state.calculateElapsedPositionMs().msToDs()
|
||||
_positionDs.value =
|
||||
event.progression.calculateElapsedPositionMs().msToDs()
|
||||
// Wait a deci-second for the next position tick.
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRepeatChanged(repeatMode: RepeatMode) {
|
||||
_repeatMode.value = repeatMode
|
||||
is PlaybackEvent.RepeatModeChanged -> {
|
||||
logD("Repeat mode changed, updating current mode")
|
||||
_repeatMode.value = event.repeatMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBarActionChanged() {
|
||||
|
@ -223,8 +225,7 @@ constructor(
|
|||
playFromGenreImpl(song, genre, isImplicitlyShuffled())
|
||||
}
|
||||
|
||||
private fun isImplicitlyShuffled() =
|
||||
playbackManager.queue.isShuffled && playbackSettings.keepShuffle
|
||||
private fun isImplicitlyShuffled() = playbackManager.isShuffled && playbackSettings.keepShuffle
|
||||
|
||||
private fun playWithImpl(song: Song, with: PlaySong, shuffled: Boolean) {
|
||||
when (with) {
|
||||
|
@ -416,9 +417,9 @@ constructor(
|
|||
*
|
||||
* @param action The [InternalPlayer.Action] to perform eventually.
|
||||
*/
|
||||
fun startAction(action: InternalPlayer.Action) {
|
||||
fun playDeferred(action: DeferredPlayback) {
|
||||
logD("Starting action $action")
|
||||
playbackManager.startAction(action)
|
||||
playbackManager.playDeferred(action)
|
||||
}
|
||||
|
||||
// --- PLAYER FUNCTIONS ---
|
||||
|
@ -572,13 +573,13 @@ constructor(
|
|||
/** Toggle [isPlaying] (i.e from playing to paused) */
|
||||
fun togglePlaying() {
|
||||
logD("Toggling playing state")
|
||||
playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
|
||||
playbackManager.playing(!playbackManager.progression.isPlaying)
|
||||
}
|
||||
|
||||
/** Toggle [isShuffled] (ex. from on to off) */
|
||||
fun toggleShuffled() {
|
||||
logD("Toggling shuffled state")
|
||||
playbackManager.reorder(!playbackManager.queue.isShuffled)
|
||||
playbackManager.reorder(!playbackManager.isShuffled)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -588,7 +589,7 @@ constructor(
|
|||
*/
|
||||
fun toggleRepeatMode() {
|
||||
logD("Toggling repeat mode")
|
||||
playbackManager.repeatMode = playbackManager.repeatMode.increment()
|
||||
playbackManager.repeatMode(playbackManager.repeatMode.increment())
|
||||
}
|
||||
|
||||
// --- UI CONTROL ---
|
||||
|
|
|
@ -21,7 +21,6 @@ package org.oxycblt.auxio.playback.persist
|
|||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.playback.queue.Queue
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
@ -76,17 +75,19 @@ constructor(
|
|||
val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent }
|
||||
logD("Successfully read playback state")
|
||||
|
||||
return PlaybackStateManager.SavedState(
|
||||
parent = parent,
|
||||
queueState =
|
||||
Queue.SavedState(
|
||||
heap.map { deviceLibrary.findSong(it.uid) },
|
||||
orderedMapping,
|
||||
shuffledMapping,
|
||||
playbackState.index,
|
||||
playbackState.songUid),
|
||||
positionMs = playbackState.positionMs,
|
||||
repeatMode = playbackState.repeatMode)
|
||||
// return PlaybackStateManager.SavedState(
|
||||
// parent = parent,
|
||||
// queueState =
|
||||
// Queue.SavedState(
|
||||
// heap.map { deviceLibrary.findSong(it.uid) },
|
||||
// orderedMapping,
|
||||
// shuffledMapping,
|
||||
// playbackState.index,
|
||||
// playbackState.songUid),
|
||||
// positionMs = playbackState.positionMs,
|
||||
// repeatMode = playbackState.repeatMode)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean {
|
||||
|
|
|
@ -24,9 +24,10 @@ 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
|
||||
import org.oxycblt.auxio.util.MutableEvent
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -51,7 +52,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
|
|||
val scrollTo: Event<Int>
|
||||
get() = _scrollTo
|
||||
|
||||
private val _index = MutableStateFlow(playbackManager.queue.index)
|
||||
private val _index = MutableStateFlow(playbackManager.resolveQueue().index)
|
||||
/** The index of the currently playing song in the queue. */
|
||||
val index: StateFlow<Int>
|
||||
get() = _index
|
||||
|
@ -60,40 +61,43 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
|
|||
playbackManager.addListener(this)
|
||||
}
|
||||
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||
when (event) {
|
||||
is PlaybackEvent.IndexMoved -> {
|
||||
logD("Index moved, synchronizing and scrolling to new position")
|
||||
_scrollTo.put(queue.index)
|
||||
_index.value = queue.index
|
||||
_scrollTo.put(event.index)
|
||||
_index.value = event.index
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
|
||||
is PlaybackEvent.QueueChanged -> {
|
||||
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
|
||||
logD("Updating queue display")
|
||||
_queueInstructions.put(change.instructions)
|
||||
_queue.value = queue.resolve()
|
||||
if (change.type != Queue.Change.Type.MAPPING) {
|
||||
_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 = queue.index
|
||||
_index.value = event.queue.index
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQueueReordered(queue: Queue) {
|
||||
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(queue.index)
|
||||
_queue.value = queue.resolve()
|
||||
_index.value = queue.index
|
||||
_scrollTo.put(event.queue.index)
|
||||
_queue.value = event.queue.queue
|
||||
_index.value = event.queue.index
|
||||
}
|
||||
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||
is PlaybackEvent.NewPlayback -> {
|
||||
// Entirely new queue -> Replace queue, update index
|
||||
logD("New playback, replacing queue and position")
|
||||
_queueInstructions.put(UpdateInstructions.Replace(0))
|
||||
_scrollTo.put(queue.index)
|
||||
_queue.value = queue.resolve()
|
||||
_index.value = queue.index
|
||||
_scrollTo.put(event.queue.index)
|
||||
_queue.value = event.queue.queue
|
||||
_index.value = event.queue.index
|
||||
}
|
||||
is PlaybackEvent.RepeatModeChanged,
|
||||
is PlaybackEvent.ProgressionChanged -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
|
@ -27,11 +27,11 @@ 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.queue.Queue
|
||||
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,26 +70,35 @@ constructor(
|
|||
|
||||
// --- OVERRIDES ---
|
||||
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||
when (event) {
|
||||
is PlaybackEvent.IndexMoved -> {
|
||||
logD("Index moved, updating current song")
|
||||
applyReplayGain(queue.currentSong)
|
||||
applyReplayGain(event.currentSong)
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
|
||||
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 (change.type == Queue.Change.Type.SONG) {
|
||||
applyReplayGain(queue.currentSong)
|
||||
if (event.change.type == QueueChange.Type.SONG) {
|
||||
applyReplayGain(event.currentSong)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||
is PlaybackEvent.NewPlayback -> {
|
||||
logD("New playback started, updating playback information")
|
||||
applyReplayGain(queue.currentSong)
|
||||
applyReplayGain(event.currentSong)
|
||||
}
|
||||
is PlaybackEvent.ProgressionChanged,
|
||||
is PlaybackEvent.QueueReordered,
|
||||
is PlaybackEvent.RepeatModeChanged -> {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReplayGainSettingsChanged() {
|
||||
// ReplayGain config changed, we need to set it up again.
|
||||
applyReplayGain(playbackManager.queue.currentSong)
|
||||
applyReplayGain(playbackManager.currentSong)
|
||||
}
|
||||
|
||||
// --- REPLAYGAIN PARSING ---
|
||||
|
@ -131,7 +140,7 @@ constructor(
|
|||
logD("Using dynamic strategy")
|
||||
gain.album?.takeIf {
|
||||
playbackManager.parent is Album &&
|
||||
playbackManager.queue.currentSong?.album == playbackManager.parent
|
||||
playbackManager.currentSong?.album == playbackManager.parent
|
||||
}
|
||||
?: gain.track
|
||||
}
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
* InternalPlayer.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.state
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/**
|
||||
* An interface for internal audio playback. This can be used to coordinate what occurs in the
|
||||
* background playback task.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface InternalPlayer {
|
||||
/** The ID of the audio session started by this instance. */
|
||||
val audioSessionId: Int
|
||||
|
||||
/** Whether the player should rewind before skipping back. */
|
||||
val shouldRewindWithPrev: Boolean
|
||||
|
||||
/**
|
||||
* Load a new [Song] into the internal player.
|
||||
*
|
||||
* @param song The [Song] to load, or null if playback should stop entirely.
|
||||
* @param play Whether to start playing when the [Song] is loaded.
|
||||
*/
|
||||
fun loadSong(song: Song?, play: Boolean)
|
||||
|
||||
/**
|
||||
* Called when an [Action] has been queued and this [InternalPlayer] is available to handle it.
|
||||
*
|
||||
* @param action The [Action] to perform.
|
||||
* @return true if the action was handled, false otherwise.
|
||||
*/
|
||||
fun performAction(action: Action): Boolean
|
||||
|
||||
/**
|
||||
* Get a [State] corresponding to the current player state.
|
||||
*
|
||||
* @param durationMs The duration of the currently playing track, in milliseconds. Required
|
||||
* since the internal player cannot obtain an accurate duration itself.
|
||||
*/
|
||||
fun getState(durationMs: Long): State
|
||||
|
||||
/**
|
||||
* Seek to a given position in the internal player.
|
||||
*
|
||||
* @param positionMs The position to seek to, in milliseconds.
|
||||
*/
|
||||
fun seekTo(positionMs: Long)
|
||||
|
||||
/**
|
||||
* Set whether the player should play or not.
|
||||
*
|
||||
* @param isPlaying Whether to play or pause the current playback.
|
||||
*/
|
||||
fun setPlaying(isPlaying: Boolean)
|
||||
|
||||
/** Possible long-running background tasks handled by the background playback task. */
|
||||
sealed interface Action {
|
||||
/** Restore the previously saved playback state. */
|
||||
data object RestoreState : Action
|
||||
|
||||
/**
|
||||
* Start shuffled playback of the entire music library. Analogous to the "Shuffle All"
|
||||
* shortcut.
|
||||
*/
|
||||
data object ShuffleAll : Action
|
||||
|
||||
/**
|
||||
* Start playing an audio file at the given [Uri].
|
||||
*
|
||||
* @param uri The [Uri] of the audio file to start playing.
|
||||
*/
|
||||
data class Open(val uri: Uri) : Action
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the current state of audio playback. Use [from] to create an instance.
|
||||
*/
|
||||
class State
|
||||
private constructor(
|
||||
/** Whether the player is actively playing audio or set to play audio in the future. */
|
||||
val isPlaying: Boolean,
|
||||
/** Whether the player is actively playing audio in this moment. */
|
||||
private val isAdvancing: Boolean,
|
||||
/** The position when this instance was created, in milliseconds. */
|
||||
private val initPositionMs: Long,
|
||||
/** The time this instance was created, as a unix epoch timestamp. */
|
||||
private val creationTime: Long
|
||||
) {
|
||||
/**
|
||||
* Calculate the "real" playback position this instance contains, in milliseconds.
|
||||
*
|
||||
* @return If paused, the original position will be returned. Otherwise, it will be the
|
||||
* original position plus the time elapsed since this state was created.
|
||||
*/
|
||||
fun calculateElapsedPositionMs() =
|
||||
if (isAdvancing) {
|
||||
initPositionMs + (SystemClock.elapsedRealtime() - creationTime)
|
||||
} else {
|
||||
// Not advancing due to buffering or some unrelated pausing, such as
|
||||
// a transient audio focus change.
|
||||
initPositionMs
|
||||
}
|
||||
|
||||
/**
|
||||
* Load this instance into a [PlaybackStateCompat].
|
||||
*
|
||||
* @param builder The [PlaybackStateCompat.Builder] to mutate.
|
||||
* @return The same [PlaybackStateCompat.Builder] for easy chaining.
|
||||
*/
|
||||
fun intoPlaybackState(builder: PlaybackStateCompat.Builder): PlaybackStateCompat.Builder =
|
||||
builder.setState(
|
||||
// State represents the user's preference, not the actual player state.
|
||||
// Doing this produces a better experience in the media control UI.
|
||||
if (isPlaying) {
|
||||
PlaybackStateCompat.STATE_PLAYING
|
||||
} else {
|
||||
PlaybackStateCompat.STATE_PAUSED
|
||||
},
|
||||
initPositionMs,
|
||||
if (isAdvancing) {
|
||||
1f
|
||||
} else {
|
||||
// Not advancing, so don't move the position.
|
||||
0f
|
||||
},
|
||||
creationTime)
|
||||
|
||||
// Equality ignores the creation time to prevent functionally identical states
|
||||
// from being non-equal.
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is State &&
|
||||
isPlaying == other.isPlaying &&
|
||||
isAdvancing == other.isAdvancing &&
|
||||
initPositionMs == other.initPositionMs
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = isPlaying.hashCode()
|
||||
result = 31 * result + isAdvancing.hashCode()
|
||||
result = 31 * result + initPositionMs.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param isPlaying Whether the player is actively playing audio or set to play audio in
|
||||
* the future.
|
||||
* @param isAdvancing Whether the player is actively playing audio in this moment.
|
||||
* @param positionMs The current position of the player.
|
||||
*/
|
||||
fun from(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) =
|
||||
State(
|
||||
isPlaying,
|
||||
// Minor sanity check: Make sure that advancing can't occur if already paused.
|
||||
isPlaying && isAdvancing,
|
||||
positionMs,
|
||||
SystemClock.elapsedRealtime())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* PlaybackStateHolder.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.state
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
interface PlaybackStateHolder {
|
||||
val currentSong: Song?
|
||||
|
||||
val repeatMode: RepeatMode
|
||||
|
||||
val progression: Progression
|
||||
|
||||
val audioSessionId: Int
|
||||
|
||||
val parent: MusicParent?
|
||||
|
||||
val isShuffled: Boolean
|
||||
|
||||
fun resolveQueue(): Queue
|
||||
|
||||
fun newPlayback(
|
||||
queue: List<Song>,
|
||||
start: Song?,
|
||||
parent: MusicParent?,
|
||||
shuffled: Boolean,
|
||||
play: Boolean
|
||||
)
|
||||
|
||||
fun playing(playing: Boolean)
|
||||
|
||||
fun seekTo(positionMs: Long)
|
||||
|
||||
fun repeatMode(repeatMode: RepeatMode)
|
||||
|
||||
fun next()
|
||||
|
||||
fun prev()
|
||||
|
||||
fun goto(index: Int)
|
||||
|
||||
fun playNext(songs: List<Song>)
|
||||
|
||||
fun addToQueue(songs: List<Song>)
|
||||
|
||||
fun move(from: Int, to: Int)
|
||||
|
||||
fun remove(at: Int)
|
||||
|
||||
fun reorder(shuffled: Boolean)
|
||||
|
||||
fun handleDeferred(action: DeferredPlayback): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 QueueChange(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
|
||||
}
|
||||
}
|
||||
|
||||
/** Possible long-running background tasks handled by the background playback task. */
|
||||
sealed interface DeferredPlayback {
|
||||
/** Restore the previously saved playback state. */
|
||||
data object RestoreState : DeferredPlayback
|
||||
|
||||
/**
|
||||
* Start shuffled playback of the entire music library. Analogous to the "Shuffle All" shortcut.
|
||||
*/
|
||||
data object ShuffleAll : DeferredPlayback
|
||||
|
||||
/**
|
||||
* Start playing an audio file at the given [Uri].
|
||||
*
|
||||
* @param uri The [Uri] of the audio file to start playing.
|
||||
*/
|
||||
data class Open(val uri: Uri) : DeferredPlayback
|
||||
}
|
||||
|
||||
data class Queue(val index: Int, val queue: List<Song>) {
|
||||
companion object {
|
||||
fun nil() = Queue(-1, emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
data class SavedQueue(
|
||||
val heap: List<Song?>,
|
||||
val orderedMapping: List<Int>,
|
||||
val shuffledMapping: List<Int>,
|
||||
val index: Int,
|
||||
val songUid: Music.UID,
|
||||
)
|
||||
|
||||
/** A representation of the current state of audio playback. Use [from] to create an instance. */
|
||||
class Progression
|
||||
private constructor(
|
||||
/** Whether the player is actively playing audio or set to play audio in the future. */
|
||||
val isPlaying: Boolean,
|
||||
/** Whether the player is actively playing audio in this moment. */
|
||||
private val isAdvancing: Boolean,
|
||||
/** The position when this instance was created, in milliseconds. */
|
||||
private val initPositionMs: Long,
|
||||
/** The time this instance was created, as a unix epoch timestamp. */
|
||||
private val creationTime: Long
|
||||
) {
|
||||
/**
|
||||
* Calculate the "real" playback position this instance contains, in milliseconds.
|
||||
*
|
||||
* @return If paused, the original position will be returned. Otherwise, it will be the original
|
||||
* position plus the time elapsed since this state was created.
|
||||
*/
|
||||
fun calculateElapsedPositionMs() =
|
||||
if (isAdvancing) {
|
||||
initPositionMs + (SystemClock.elapsedRealtime() - creationTime)
|
||||
} else {
|
||||
// Not advancing due to buffering or some unrelated pausing, such as
|
||||
// a transient audio focus change.
|
||||
initPositionMs
|
||||
}
|
||||
|
||||
/**
|
||||
* Load this instance into a [PlaybackStateCompat].
|
||||
*
|
||||
* @param builder The [PlaybackStateCompat.Builder] to mutate.
|
||||
* @return The same [PlaybackStateCompat.Builder] for easy chaining.
|
||||
*/
|
||||
fun intoPlaybackState(builder: PlaybackStateCompat.Builder): PlaybackStateCompat.Builder =
|
||||
builder.setState(
|
||||
// State represents the user's preference, not the actual player state.
|
||||
// Doing this produces a better experience in the media control UI.
|
||||
if (isPlaying) {
|
||||
PlaybackStateCompat.STATE_PLAYING
|
||||
} else {
|
||||
PlaybackStateCompat.STATE_PAUSED
|
||||
},
|
||||
initPositionMs,
|
||||
if (isAdvancing) {
|
||||
1f
|
||||
} else {
|
||||
// Not advancing, so don't move the position.
|
||||
0f
|
||||
},
|
||||
creationTime)
|
||||
|
||||
// Equality ignores the creation time to prevent functionally identical states
|
||||
// from being non-equal.
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is Progression &&
|
||||
isPlaying == other.isPlaying &&
|
||||
isAdvancing == other.isAdvancing &&
|
||||
initPositionMs == other.initPositionMs
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = isPlaying.hashCode()
|
||||
result = 31 * result + isAdvancing.hashCode()
|
||||
result = 31 * result + initPositionMs.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param isPlaying Whether the player is actively playing audio or set to play audio in the
|
||||
* future.
|
||||
* @param isAdvancing Whether the player is actively playing audio in this moment.
|
||||
* @param positionMs The current position of the player.
|
||||
*/
|
||||
fun from(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) =
|
||||
Progression(
|
||||
isPlaying,
|
||||
// Minor sanity check: Make sure that advancing can't occur if already paused.
|
||||
isPlaying && isAdvancing,
|
||||
positionMs,
|
||||
SystemClock.elapsedRealtime())
|
||||
|
||||
fun nil() = Progression(false, false, 0, SystemClock.elapsedRealtime())
|
||||
}
|
||||
}
|
|
@ -22,8 +22,6 @@ import javax.inject.Inject
|
|||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.queue.MutableQueue
|
||||
import org.oxycblt.auxio.playback.queue.Queue
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
|
@ -40,19 +38,26 @@ import org.oxycblt.auxio.util.logW
|
|||
* PlaybackService.
|
||||
*
|
||||
* Internal consumers should usually use [Listener], however the component that manages the player
|
||||
* itself should instead use [InternalPlayer].
|
||||
* itself should instead use [PlaybackStateHolder].
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface PlaybackStateManager {
|
||||
/** The current [Queue]. */
|
||||
val queue: Queue
|
||||
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
||||
/** The current [Progression] state. */
|
||||
val progression: Progression
|
||||
|
||||
val currentSong: Song?
|
||||
|
||||
val repeatMode: RepeatMode
|
||||
|
||||
val audioSessionId: Int
|
||||
|
||||
val parent: MusicParent?
|
||||
/** The current [InternalPlayer] state. */
|
||||
val playerState: InternalPlayer.State
|
||||
/** The current [RepeatMode] */
|
||||
var repeatMode: RepeatMode
|
||||
|
||||
val isShuffled: Boolean
|
||||
|
||||
fun resolveQueue(): Queue
|
||||
|
||||
/** The audio session ID of the internal player. Null if no internal player exists. */
|
||||
val currentAudioSessionId: Int?
|
||||
|
||||
|
@ -75,23 +80,25 @@ interface PlaybackStateManager {
|
|||
fun removeListener(listener: Listener)
|
||||
|
||||
/**
|
||||
* Register an [InternalPlayer] for this instance. This instance will handle translating the
|
||||
* current playback state into audio playback. There can be only one [InternalPlayer] at a time.
|
||||
* Will invoke [InternalPlayer] methods to initialize the instance with the current state.
|
||||
* Register an [PlaybackStateHolder] for this instance. This instance will handle translating
|
||||
* the current playback state into audio playback. There can be only one [PlaybackStateHolder]
|
||||
* at a time. Will invoke [PlaybackStateHolder] methods to initialize the instance with the
|
||||
* current state.
|
||||
*
|
||||
* @param internalPlayer The [InternalPlayer] to register. Will do nothing if already
|
||||
* @param stateHolder The [PlaybackStateHolder] to register. Will do nothing if already
|
||||
* registered.
|
||||
*/
|
||||
fun registerInternalPlayer(internalPlayer: InternalPlayer)
|
||||
fun registerStateHolder(stateHolder: PlaybackStateHolder)
|
||||
|
||||
/**
|
||||
* Unregister the [InternalPlayer] from this instance, prevent it from receiving any further
|
||||
* commands.
|
||||
* Unregister the [PlaybackStateHolder] from this instance, prevent it from receiving any
|
||||
* further commands.
|
||||
*
|
||||
* @param internalPlayer The [InternalPlayer] to unregister. Must be the current
|
||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
||||
* @param stateHolder The [PlaybackStateHolder] to unregister. Must be the current
|
||||
* [PlaybackStateHolder]. Does nothing if invoked by another [PlaybackStateHolder]
|
||||
* implementation.
|
||||
*/
|
||||
fun unregisterInternalPlayer(internalPlayer: InternalPlayer)
|
||||
fun unregisterStateHolder(stateHolder: PlaybackStateHolder)
|
||||
|
||||
/**
|
||||
* Start new playback.
|
||||
|
@ -173,36 +180,33 @@ interface PlaybackStateManager {
|
|||
*/
|
||||
fun reorder(shuffled: Boolean)
|
||||
|
||||
/**
|
||||
* Synchronize the state of this instance with the current [InternalPlayer].
|
||||
*
|
||||
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
||||
*/
|
||||
fun synchronizeState(internalPlayer: InternalPlayer)
|
||||
fun dispatchEvent(stateHolder: PlaybackStateHolder, vararg events: PlaybackEvent)
|
||||
|
||||
/**
|
||||
* Start a [InternalPlayer.Action] for the current [InternalPlayer] to handle eventually.
|
||||
* Start a [DeferredPlayback] for the current [PlaybackStateHolder] to handle eventually.
|
||||
*
|
||||
* @param action The [InternalPlayer.Action] to perform.
|
||||
* @param action The [DeferredPlayback] to perform.
|
||||
*/
|
||||
fun startAction(action: InternalPlayer.Action)
|
||||
fun playDeferred(action: DeferredPlayback)
|
||||
|
||||
/**
|
||||
* Request that the pending [InternalPlayer.Action] (if any) be passed to the given
|
||||
* [InternalPlayer].
|
||||
* Request that the pending [PlaybackStateHolder.Action] (if any) be passed to the given
|
||||
* [PlaybackStateHolder].
|
||||
*
|
||||
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
||||
* @param stateHolder The [PlaybackStateHolder] to synchronize with. Must be the current
|
||||
* [PlaybackStateHolder]. Does nothing if invoked by another [PlaybackStateHolder]
|
||||
* implementation.
|
||||
*/
|
||||
fun requestAction(internalPlayer: InternalPlayer)
|
||||
fun requestAction(stateHolder: PlaybackStateHolder)
|
||||
|
||||
/**
|
||||
* Update whether playback is ongoing or not.
|
||||
*
|
||||
* @param isPlaying Whether playback is ongoing or not.
|
||||
*/
|
||||
fun setPlaying(isPlaying: Boolean)
|
||||
fun playing(isPlaying: Boolean)
|
||||
|
||||
fun repeatMode(repeatMode: RepeatMode)
|
||||
|
||||
/**
|
||||
* Seek to the given position in the currently playing [Song].
|
||||
|
@ -236,103 +240,78 @@ interface PlaybackStateManager {
|
|||
* [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener].
|
||||
*/
|
||||
interface Listener {
|
||||
/**
|
||||
* Called when the position of the currently playing item has changed, changing the current
|
||||
* [Song], but no other queue attribute has changed.
|
||||
*
|
||||
* @param queue The new [Queue].
|
||||
*/
|
||||
fun onIndexMoved(queue: Queue) {}
|
||||
|
||||
/**
|
||||
* 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: Queue, change: Queue.Change) {}
|
||||
|
||||
/**
|
||||
* 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: Queue) {}
|
||||
|
||||
/**
|
||||
* 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(queue: Queue, parent: MusicParent?) {}
|
||||
|
||||
/**
|
||||
* Called when the state of the [InternalPlayer] changes.
|
||||
*
|
||||
* @param state The new state of the [InternalPlayer].
|
||||
*/
|
||||
fun onStateChanged(state: InternalPlayer.State) {}
|
||||
|
||||
/**
|
||||
* Called when the [RepeatMode] changes.
|
||||
*
|
||||
* @param repeatMode The new [RepeatMode].
|
||||
*/
|
||||
fun onRepeatChanged(repeatMode: RepeatMode) {}
|
||||
fun onPlaybackEvent(event: PlaybackEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* A condensed representation of the playback state that can be persisted.
|
||||
*
|
||||
* @param parent The [MusicParent] item currently being played from.
|
||||
* @param queueState The [Queue.SavedState]
|
||||
* @param queueState The [SavedQueue]
|
||||
* @param positionMs The current position in the currently played song, in ms
|
||||
* @param repeatMode The current [RepeatMode].
|
||||
*/
|
||||
data class SavedState(
|
||||
val parent: MusicParent?,
|
||||
val queueState: Queue.SavedState,
|
||||
val queueState: SavedQueue,
|
||||
val positionMs: Long,
|
||||
val repeatMode: RepeatMode,
|
||||
)
|
||||
}
|
||||
|
||||
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 val listeners = mutableListOf<PlaybackStateManager.Listener>()
|
||||
@Volatile private var internalPlayer: InternalPlayer? = null
|
||||
@Volatile private var pendingAction: InternalPlayer.Action? = null
|
||||
@Volatile private var stateHolder: PlaybackStateHolder? = null
|
||||
@Volatile private var pendingDeferredPlayback: DeferredPlayback? = null
|
||||
@Volatile private var isInitialized = false
|
||||
|
||||
override val queue = MutableQueue()
|
||||
@Volatile
|
||||
override var parent: MusicParent? = null
|
||||
private set
|
||||
/** The current [Progression] state. */
|
||||
override val progression: Progression
|
||||
get() = stateHolder?.progression ?: Progression.nil()
|
||||
|
||||
@Volatile
|
||||
override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
|
||||
private set
|
||||
override val currentSong: Song?
|
||||
get() = stateHolder?.currentSong
|
||||
|
||||
@Volatile
|
||||
override var repeatMode = RepeatMode.NONE
|
||||
set(value) {
|
||||
field = value
|
||||
notifyRepeatModeChanged()
|
||||
}
|
||||
override val repeatMode: RepeatMode
|
||||
get() = stateHolder?.repeatMode ?: RepeatMode.NONE
|
||||
|
||||
override val audioSessionId: Int
|
||||
get() = stateHolder?.audioSessionId ?: -1
|
||||
|
||||
override val parent: MusicParent?
|
||||
get() = stateHolder?.parent
|
||||
|
||||
override val isShuffled: Boolean
|
||||
get() = stateHolder?.isShuffled ?: false
|
||||
|
||||
override fun resolveQueue() = stateHolder?.resolveQueue() ?: Queue.nil()
|
||||
|
||||
override val currentAudioSessionId: Int?
|
||||
get() = internalPlayer?.audioSessionId
|
||||
get() = stateHolder?.audioSessionId
|
||||
|
||||
@Synchronized
|
||||
override fun addListener(listener: PlaybackStateManager.Listener) {
|
||||
logD("Adding $listener to listeners")
|
||||
if (isInitialized) {
|
||||
listener.onNewPlayback(queue, parent)
|
||||
listener.onRepeatChanged(repeatMode)
|
||||
listener.onStateChanged(playerState)
|
||||
}
|
||||
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
|
@ -345,286 +324,211 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
override fun registerInternalPlayer(internalPlayer: InternalPlayer) {
|
||||
if (this.internalPlayer != null) {
|
||||
override fun registerStateHolder(stateHolder: PlaybackStateHolder) {
|
||||
if (this.stateHolder != null) {
|
||||
logW("Internal player is already registered")
|
||||
return
|
||||
}
|
||||
|
||||
logD("Registering internal player $internalPlayer")
|
||||
|
||||
if (isInitialized) {
|
||||
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
|
||||
internalPlayer.seekTo(playerState.calculateElapsedPositionMs())
|
||||
// See if there's any action that has been queued.
|
||||
requestAction(internalPlayer)
|
||||
// Once initialized, try to synchronize with the player state it has created.
|
||||
synchronizeState(internalPlayer)
|
||||
}
|
||||
|
||||
this.internalPlayer = internalPlayer
|
||||
this.stateHolder = stateHolder
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun unregisterInternalPlayer(internalPlayer: InternalPlayer) {
|
||||
if (this.internalPlayer !== internalPlayer) {
|
||||
override fun unregisterStateHolder(stateHolder: PlaybackStateHolder) {
|
||||
if (this.stateHolder !== stateHolder) {
|
||||
logW("Given internal player did not match current internal player")
|
||||
return
|
||||
}
|
||||
|
||||
logD("Unregistering internal player $internalPlayer")
|
||||
logD("Unregistering internal player $stateHolder")
|
||||
|
||||
this.internalPlayer = null
|
||||
this.stateHolder = null
|
||||
}
|
||||
|
||||
// --- PLAYING FUNCTIONS ---
|
||||
|
||||
@Synchronized
|
||||
override fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean) {
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Playing $song from $parent in ${queue.size}-song queue [shuffled=$shuffled]")
|
||||
// Set up parent and queue
|
||||
this.parent = parent
|
||||
this.queue.start(song, queue, shuffled)
|
||||
// Notify components of changes
|
||||
notifyNewPlayback()
|
||||
internalPlayer.loadSong(this.queue.currentSong, true)
|
||||
// Played something, so we are initialized now
|
||||
isInitialized = true
|
||||
stateHolder.newPlayback(queue, song, parent, shuffled, true)
|
||||
}
|
||||
|
||||
// --- QUEUE FUNCTIONS ---
|
||||
|
||||
@Synchronized
|
||||
override fun next() {
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
var play = true
|
||||
if (!queue.goto(queue.index + 1)) {
|
||||
queue.goto(0)
|
||||
play = repeatMode == RepeatMode.ALL
|
||||
logD("At end of queue, wrapping around to position 0 [play=$play]")
|
||||
} else {
|
||||
logD("Moving to next song")
|
||||
}
|
||||
notifyIndexMoved()
|
||||
internalPlayer.loadSong(queue.currentSong, play)
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Going to next song")
|
||||
stateHolder.next()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun prev() {
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
|
||||
if (internalPlayer.shouldRewindWithPrev) {
|
||||
logD("Rewinding current song")
|
||||
rewind()
|
||||
setPlaying(true)
|
||||
} else {
|
||||
logD("Moving to previous song")
|
||||
if (!queue.goto(queue.index - 1)) {
|
||||
queue.goto(0)
|
||||
}
|
||||
notifyIndexMoved()
|
||||
internalPlayer.loadSong(queue.currentSong, true)
|
||||
}
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Going to previous song")
|
||||
stateHolder.prev()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun goto(index: Int) {
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
if (queue.goto(index)) {
|
||||
logD("Moving to $index")
|
||||
notifyIndexMoved()
|
||||
internalPlayer.loadSong(queue.currentSong, true)
|
||||
} else {
|
||||
logW("$index was not in bounds, could not move to it")
|
||||
}
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Going to index $index")
|
||||
stateHolder.goto(index)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun playNext(songs: List<Song>) {
|
||||
if (queue.currentSong == null) {
|
||||
if (currentSong == null) {
|
||||
logD("Nothing playing, short-circuiting to new playback")
|
||||
play(songs[0], null, songs, false)
|
||||
} else {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Adding ${songs.size} songs to start of queue")
|
||||
notifyQueueChanged(queue.addToTop(songs))
|
||||
stateHolder.playNext(songs)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun addToQueue(songs: List<Song>) {
|
||||
if (queue.currentSong == null) {
|
||||
if (currentSong == null) {
|
||||
logD("Nothing playing, short-circuiting to new playback")
|
||||
play(songs[0], null, songs, false)
|
||||
} else {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Adding ${songs.size} songs to end of queue")
|
||||
notifyQueueChanged(queue.addToBottom(songs))
|
||||
stateHolder.addToQueue(songs)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun moveQueueItem(src: Int, dst: Int) {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Moving item $src to position $dst")
|
||||
notifyQueueChanged(queue.move(src, dst))
|
||||
stateHolder.move(src, dst)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun removeQueueItem(at: Int) {
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Removing item at $at")
|
||||
val change = queue.remove(at)
|
||||
if (change.type == Queue.Change.Type.SONG) {
|
||||
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
|
||||
}
|
||||
notifyQueueChanged(change)
|
||||
stateHolder.remove(at)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun reorder(shuffled: Boolean) {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Reordering queue [shuffled=$shuffled]")
|
||||
queue.reorder(shuffled)
|
||||
notifyQueueReordered()
|
||||
stateHolder.reorder(shuffled)
|
||||
}
|
||||
|
||||
// --- INTERNAL PLAYER FUNCTIONS ---
|
||||
|
||||
@Synchronized
|
||||
override fun synchronizeState(internalPlayer: InternalPlayer) {
|
||||
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
|
||||
logW("Given internal player did not match current internal player")
|
||||
return
|
||||
}
|
||||
|
||||
val newState = internalPlayer.getState(queue.currentSong?.durationMs ?: 0)
|
||||
if (newState != playerState) {
|
||||
playerState = newState
|
||||
notifyStateChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun startAction(action: InternalPlayer.Action) {
|
||||
val internalPlayer = internalPlayer
|
||||
if (internalPlayer == null || !internalPlayer.performAction(action)) {
|
||||
override fun playDeferred(action: DeferredPlayback) {
|
||||
val stateHolder = stateHolder
|
||||
if (stateHolder == null || !stateHolder.handleDeferred(action)) {
|
||||
logD("Internal player not present or did not consume action, waiting")
|
||||
pendingAction = action
|
||||
pendingDeferredPlayback = action
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun requestAction(internalPlayer: InternalPlayer) {
|
||||
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
|
||||
override fun requestAction(stateHolder: PlaybackStateHolder) {
|
||||
if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) {
|
||||
logW("Given internal player did not match current internal player")
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingAction?.let(internalPlayer::performAction) == true) {
|
||||
if (pendingDeferredPlayback?.let(stateHolder::handleDeferred) == true) {
|
||||
logD("Pending action consumed")
|
||||
pendingAction = null
|
||||
pendingDeferredPlayback = null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun setPlaying(isPlaying: Boolean) {
|
||||
override fun playing(isPlaying: Boolean) {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Updating playing state to $isPlaying")
|
||||
internalPlayer?.setPlaying(isPlaying)
|
||||
stateHolder.playing(isPlaying)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun repeatMode(repeatMode: RepeatMode) {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Updating repeat mode to $repeatMode")
|
||||
stateHolder.repeatMode(repeatMode)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun seekTo(positionMs: Long) {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Seeking to ${positionMs}ms")
|
||||
internalPlayer?.seekTo(positionMs)
|
||||
stateHolder.seekTo(positionMs)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun dispatchEvent(stateHolder: PlaybackStateHolder, vararg events: PlaybackEvent) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
// --- PERSISTENCE FUNCTIONS ---
|
||||
|
||||
@Synchronized
|
||||
override fun toSavedState() =
|
||||
queue.toSavedState()?.let {
|
||||
PlaybackStateManager.SavedState(
|
||||
parent = parent,
|
||||
queueState = it,
|
||||
positionMs = playerState.calculateElapsedPositionMs(),
|
||||
repeatMode = repeatMode)
|
||||
}
|
||||
@Synchronized override fun toSavedState() = null
|
||||
// queue.toSavedState()?.let {
|
||||
// PlaybackStateManager.SavedState(
|
||||
// parent = parent,
|
||||
// queueState = it,
|
||||
// positionMs = progression.calculateElapsedPositionMs(),
|
||||
// repeatMode = repeatMode)
|
||||
// }
|
||||
|
||||
@Synchronized
|
||||
override fun applySavedState(
|
||||
savedState: PlaybackStateManager.SavedState,
|
||||
destructive: Boolean
|
||||
) {
|
||||
if (isInitialized && !destructive) {
|
||||
logW("Already initialized, cannot apply saved state")
|
||||
return
|
||||
}
|
||||
val internalPlayer = internalPlayer ?: return
|
||||
logD("Applying state $savedState")
|
||||
|
||||
val lastSong = queue.currentSong
|
||||
parent = savedState.parent
|
||||
queue.applySavedState(savedState.queueState)
|
||||
repeatMode = savedState.repeatMode
|
||||
notifyNewPlayback()
|
||||
|
||||
// Check if we need to reload the player with a new music file, or if we can just leave
|
||||
// it be. Specifically done so we don't pause on music updates that don't really change
|
||||
// what's playing (ex. playlist editing)
|
||||
if (lastSong != queue.currentSong) {
|
||||
logD("Song changed, must reload player")
|
||||
// Continuing playback while also possibly doing drastic state updates is
|
||||
// a bad idea, so pause.
|
||||
internalPlayer.loadSong(queue.currentSong, false)
|
||||
if (queue.currentSong != null) {
|
||||
logD("Seeking to saved position ${savedState.positionMs}ms")
|
||||
// Internal player may have reloaded the media item, re-seek to the previous
|
||||
// position
|
||||
seekTo(savedState.positionMs)
|
||||
}
|
||||
}
|
||||
// if (isInitialized && !destructive) {
|
||||
// logW("Already initialized, cannot apply saved state")
|
||||
// return
|
||||
// }
|
||||
// val stateHolder = stateHolder ?: return
|
||||
// logD("Applying state $savedState")
|
||||
//
|
||||
// val lastSong = queue.currentSong
|
||||
// parent = savedState.parent
|
||||
// queue.applySavedState(savedState.queueState)
|
||||
// repeatMode = savedState.repeatMode
|
||||
// notifyNewPlayback()
|
||||
//
|
||||
// // Check if we need to reload the player with a new music file, or if we can just
|
||||
// leave
|
||||
// // it be. Specifically done so we don't pause on music updates that don't really
|
||||
// change
|
||||
// // what's playing (ex. playlist editing)
|
||||
// if (lastSong != queue.currentSong) {
|
||||
// logD("Song changed, must reload player")
|
||||
// // Continuing playback while also possibly doing drastic state updates is
|
||||
// // a bad idea, so pause.
|
||||
// stateHolder.loadSong(queue.currentSong, false)
|
||||
// if (queue.currentSong != null) {
|
||||
// logD("Seeking to saved position ${savedState.positionMs}ms")
|
||||
// // Internal player may have reloaded the media item, re-seek to the
|
||||
// previous
|
||||
// // position
|
||||
// seekTo(savedState.positionMs)
|
||||
// }
|
||||
// }
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
// --- CALLBACKS ---
|
||||
|
||||
private fun notifyIndexMoved() {
|
||||
logD("Dispatching index change")
|
||||
for (callback in listeners) {
|
||||
callback.onIndexMoved(queue)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyQueueChanged(change: Queue.Change) {
|
||||
logD("Dispatching queue change $change")
|
||||
for (callback in listeners) {
|
||||
callback.onQueueChanged(queue, change)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyQueueReordered() {
|
||||
logD("Dispatching queue reordering")
|
||||
for (callback in listeners) {
|
||||
callback.onQueueReordered(queue)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyNewPlayback() {
|
||||
logD("Dispatching new playback")
|
||||
for (callback in listeners) {
|
||||
callback.onNewPlayback(queue, parent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyStateChanged() {
|
||||
logD("Dispatching player state change")
|
||||
for (callback in listeners) {
|
||||
callback.onStateChanged(playerState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyRepeatModeChanged() {
|
||||
logD("Dispatching repeat mode change")
|
||||
for (callback in listeners) {
|
||||
callback.onRepeatChanged(repeatMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,18 +29,10 @@ import java.util.*
|
|||
*
|
||||
* @author media3 team, Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class BetterShuffleOrder
|
||||
private constructor(private val shuffled: IntArray, private val random: Random) : ShuffleOrder {
|
||||
class BetterShuffleOrder private constructor(private val shuffled: IntArray) : ShuffleOrder {
|
||||
private val indexInShuffled: IntArray = IntArray(shuffled.size)
|
||||
|
||||
/**
|
||||
* Creates an instance with a specified length.
|
||||
*
|
||||
* @param length The length of the shuffle order.
|
||||
*/
|
||||
constructor(length: Int) : this(length, Random())
|
||||
|
||||
constructor(length: Int, random: Random) : this(createShuffledList(length, random), random)
|
||||
constructor(length: Int, startIndex: Int) : this(createShuffledList(length, startIndex))
|
||||
|
||||
init {
|
||||
for (i in shuffled.indices) {
|
||||
|
@ -88,7 +80,7 @@ private constructor(private val shuffled: IntArray, private val random: Random)
|
|||
for (i in 0 until insertionCount) {
|
||||
newShuffled[pivot + i + 1] = insertionIndex + i + 1
|
||||
}
|
||||
return BetterShuffleOrder(newShuffled, Random(random.nextLong()))
|
||||
return BetterShuffleOrder(newShuffled)
|
||||
}
|
||||
|
||||
override fun cloneAndRemove(indexFrom: Int, indexToExclusive: Int): ShuffleOrder {
|
||||
|
@ -104,21 +96,27 @@ private constructor(private val shuffled: IntArray, private val random: Random)
|
|||
else shuffled[i]
|
||||
}
|
||||
}
|
||||
return BetterShuffleOrder(newShuffled, Random(random.nextLong()))
|
||||
return BetterShuffleOrder(newShuffled)
|
||||
}
|
||||
|
||||
override fun cloneAndClear(): ShuffleOrder {
|
||||
return BetterShuffleOrder(0, Random(random.nextLong()))
|
||||
return BetterShuffleOrder(0, -1)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun createShuffledList(length: Int, random: Random): IntArray {
|
||||
private fun createShuffledList(length: Int, startIndex: Int): IntArray {
|
||||
val shuffled = IntArray(length)
|
||||
for (i in 0 until length) {
|
||||
val swapIndex = random.nextInt(i + 1)
|
||||
val swapIndex = (0..i).random()
|
||||
shuffled[i] = shuffled[swapIndex]
|
||||
shuffled[swapIndex] = i
|
||||
}
|
||||
if (startIndex != -1) {
|
||||
val startIndexInShuffled = shuffled.indexOf(startIndex)
|
||||
val temp = shuffled[0]
|
||||
shuffled[0] = shuffled[startIndexInShuffled]
|
||||
shuffled[startIndexInShuffled] = temp
|
||||
}
|
||||
return shuffled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ExoPlayerExt.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.system
|
||||
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
val ExoPlayer.song
|
||||
get() = currentMediaItem?.song
|
||||
|
||||
fun ExoPlayer.orderedQueue(queue: Collection<Song>, start: Song?) {
|
||||
clearMediaItems()
|
||||
shuffleModeEnabled = false
|
||||
setShuffleOrder(DefaultShuffleOrder(mediaItemCount))
|
||||
setMediaItems(queue.map { it.toMediaItem() })
|
||||
val startIndex = queue.indexOf(start)
|
||||
if (startIndex != -1) {
|
||||
seekTo(startIndex, C.TIME_UNSET)
|
||||
} else {
|
||||
throw IllegalArgumentException("Start song not in queue")
|
||||
}
|
||||
}
|
||||
|
||||
fun ExoPlayer.shuffledQueue(queue: Collection<Song>, start: Song?) {
|
||||
// A fun thing about ShuffleOrder is that ExoPlayer will use cloneAndInsert to both add
|
||||
// MediaItems AND repopulate MediaItems (?!?!?!?!). As a result, we have to use the default
|
||||
// shuffle order and it's stupid cloneAndInsert implementation to add the songs, and then
|
||||
// switch back to our implementation that actually works in normal use.
|
||||
setShuffleOrder(DefaultShuffleOrder(mediaItemCount))
|
||||
setMediaItems(queue.map { it.toMediaItem() })
|
||||
shuffleModeEnabled = true
|
||||
val startIndex =
|
||||
if (start != null) {
|
||||
queue.indexOf(start).also { check(it != -1) { "Start song not in queue" } }
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
setShuffleOrder(BetterShuffleOrder(queue.size, startIndex))
|
||||
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")
|
||||
if (shuffled) {
|
||||
// Have to manually refresh the shuffle seed.
|
||||
setShuffleOrder(BetterShuffleOrder(mediaItemCount, currentMediaItemIndex))
|
||||
}
|
||||
shuffleModeEnabled = shuffled
|
||||
}
|
||||
|
||||
fun ExoPlayer.playNext(songs: List<Song>) {
|
||||
addMediaItems(nextMediaItemIndex, songs.map { it.toMediaItem() })
|
||||
}
|
||||
|
||||
fun ExoPlayer.addToQueue(songs: List<Song>) {
|
||||
addMediaItems(songs.map { it.toMediaItem() })
|
||||
}
|
||||
|
||||
fun ExoPlayer.goto(index: Int) {
|
||||
val queue = unscrambleQueue { index -> index }
|
||||
if (queue.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = queue[index]
|
||||
seekTo(trueIndex, C.TIME_UNSET)
|
||||
}
|
||||
|
||||
fun ExoPlayer.move(from: Int, to: Int) {
|
||||
val queue = unscrambleQueue { index -> index }
|
||||
if (queue.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueFrom = queue[from]
|
||||
val trueTo = queue[to]
|
||||
moveMediaItem(trueFrom, trueTo)
|
||||
}
|
||||
|
||||
fun ExoPlayer.remove(at: Int) {
|
||||
val queue = unscrambleQueue { index -> index }
|
||||
if (queue.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = queue[at]
|
||||
removeMediaItem(trueIndex)
|
||||
}
|
||||
|
||||
fun ExoPlayer.resolveQueue(): List<Song> {
|
||||
return unscrambleQueue { index -> getMediaItemAt(index).song }
|
||||
}
|
||||
|
||||
inline fun <T> ExoPlayer.unscrambleQueue(mapper: (Int) -> T): List<T> {
|
||||
val timeline = currentTimeline
|
||||
if (timeline.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val queue = mutableListOf<T>()
|
||||
|
||||
// Add the active queue item.
|
||||
val currentMediaItemIndex = currentMediaItemIndex
|
||||
queue.add(mapper(currentMediaItemIndex))
|
||||
|
||||
// Fill queue alternating with next and/or previous queue items.
|
||||
var firstMediaItemIndex = currentMediaItemIndex
|
||||
var lastMediaItemIndex = currentMediaItemIndex
|
||||
val shuffleModeEnabled = shuffleModeEnabled
|
||||
while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET)) {
|
||||
// Begin with next to have a longer tail than head if an even sized queue needs to be
|
||||
// trimmed.
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
lastMediaItemIndex =
|
||||
timeline.getNextWindowIndex(
|
||||
lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(mapper(lastMediaItemIndex))
|
||||
}
|
||||
}
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
firstMediaItemIndex =
|
||||
timeline.getPreviousWindowIndex(
|
||||
firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(0, mapper(firstMediaItemIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
fun Song.toMediaItem() = MediaItem.Builder().setUri(uri).setTag(this).build()
|
||||
|
||||
private val MediaItem.song: Song
|
||||
get() = requireNotNull(localConfiguration).tag as Song
|
|
@ -39,7 +39,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
|
|||
|
||||
// TODO: Figure this out
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (playbackManager.queue.currentSong != null) {
|
||||
if (playbackManager.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
|
||||
|
|
|
@ -38,9 +38,10 @@ 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.queue.Queue
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
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.QueueChange
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -117,52 +118,50 @@ constructor(
|
|||
|
||||
// --- PLAYBACKSTATEMANAGER OVERRIDES ---
|
||||
|
||||
override fun onIndexMoved(queue: Queue) {
|
||||
updateMediaMetadata(queue.currentSong, playbackManager.parent)
|
||||
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||
when (event) {
|
||||
is PlaybackEvent.IndexMoved -> {
|
||||
updateMediaMetadata(event.currentSong, playbackManager.parent)
|
||||
invalidateSessionState()
|
||||
}
|
||||
|
||||
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
|
||||
updateQueue(queue)
|
||||
when (change.type) {
|
||||
is PlaybackEvent.QueueChanged -> {
|
||||
updateQueue(event.queue)
|
||||
when (event.change.type) {
|
||||
// Nothing special to do with mapping changes.
|
||||
Queue.Change.Type.MAPPING -> {}
|
||||
QueueChange.Type.MAPPING -> {}
|
||||
// Index changed, ensure playback state's index changes.
|
||||
Queue.Change.Type.INDEX -> invalidateSessionState()
|
||||
QueueChange.Type.INDEX -> invalidateSessionState()
|
||||
// Song changed, ensure metadata changes.
|
||||
Queue.Change.Type.SONG -> updateMediaMetadata(queue.currentSong, playbackManager.parent)
|
||||
QueueChange.Type.SONG ->
|
||||
updateMediaMetadata(event.currentSong, playbackManager.parent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQueueReordered(queue: Queue) {
|
||||
updateQueue(queue)
|
||||
is PlaybackEvent.QueueReordered -> {
|
||||
updateQueue(event.queue)
|
||||
invalidateSessionState()
|
||||
mediaSession.setShuffleMode(
|
||||
if (queue.isShuffled) {
|
||||
if (event.isShuffled) {
|
||||
PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||
} else {
|
||||
PlaybackStateCompat.SHUFFLE_MODE_NONE
|
||||
})
|
||||
invalidateSecondaryAction()
|
||||
}
|
||||
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||
updateMediaMetadata(queue.currentSong, parent)
|
||||
updateQueue(queue)
|
||||
is PlaybackEvent.NewPlayback -> {
|
||||
updateMediaMetadata(event.currentSong, event.parent)
|
||||
updateQueue(event.queue)
|
||||
invalidateSessionState()
|
||||
}
|
||||
|
||||
override fun onStateChanged(state: InternalPlayer.State) {
|
||||
is PlaybackEvent.ProgressionChanged -> {
|
||||
invalidateSessionState()
|
||||
notification.updatePlaying(playbackManager.playerState.isPlaying)
|
||||
notification.updatePlaying(playbackManager.progression.isPlaying)
|
||||
if (!bitmapProvider.isBusy) {
|
||||
listener?.onPostNotification(notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRepeatChanged(repeatMode: RepeatMode) {
|
||||
is PlaybackEvent.RepeatModeChanged -> {
|
||||
mediaSession.setRepeatMode(
|
||||
when (repeatMode) {
|
||||
when (event.repeatMode) {
|
||||
RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE
|
||||
RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE
|
||||
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
|
||||
|
@ -170,12 +169,14 @@ constructor(
|
|||
|
||||
invalidateSecondaryAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- SETTINGS OVERRIDES ---
|
||||
|
||||
override fun onImageSettingsChanged() {
|
||||
// Need to reload the metadata cover.
|
||||
updateMediaMetadata(playbackManager.queue.currentSong, playbackManager.parent)
|
||||
updateMediaMetadata(playbackManager.currentSong, playbackManager.parent)
|
||||
}
|
||||
|
||||
override fun onNotificationActionChanged() {
|
||||
|
@ -211,11 +212,11 @@ constructor(
|
|||
}
|
||||
|
||||
override fun onPlay() {
|
||||
playbackManager.setPlaying(true)
|
||||
playbackManager.playing(true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
playbackManager.setPlaying(false)
|
||||
playbackManager.playing(false)
|
||||
}
|
||||
|
||||
override fun onSkipToNext() {
|
||||
|
@ -236,17 +237,17 @@ constructor(
|
|||
|
||||
override fun onRewind() {
|
||||
playbackManager.rewind()
|
||||
playbackManager.setPlaying(true)
|
||||
playbackManager.playing(true)
|
||||
}
|
||||
|
||||
override fun onSetRepeatMode(repeatMode: Int) {
|
||||
playbackManager.repeatMode =
|
||||
playbackManager.repeatMode(
|
||||
when (repeatMode) {
|
||||
PlaybackStateCompat.REPEAT_MODE_ALL -> RepeatMode.ALL
|
||||
PlaybackStateCompat.REPEAT_MODE_GROUP -> RepeatMode.ALL
|
||||
PlaybackStateCompat.REPEAT_MODE_ONE -> RepeatMode.TRACK
|
||||
else -> RepeatMode.NONE
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onSetShuffleMode(shuffleMode: Int) {
|
||||
|
@ -356,7 +357,7 @@ constructor(
|
|||
*/
|
||||
private fun updateQueue(queue: Queue) {
|
||||
val queueItems =
|
||||
queue.resolve().mapIndexed { i, song ->
|
||||
queue.queue.mapIndexed { i, song ->
|
||||
val description =
|
||||
MediaDescriptionCompat.Builder()
|
||||
// Media ID should not be the item index but rather the UID,
|
||||
|
@ -381,13 +382,15 @@ constructor(
|
|||
private fun invalidateSessionState() {
|
||||
logD("Updating media session playback state")
|
||||
|
||||
val queue = playbackManager.resolveQueue()
|
||||
|
||||
val state =
|
||||
// InternalPlayer.State handles position/state information.
|
||||
playbackManager.playerState
|
||||
playbackManager.progression
|
||||
.intoPlaybackState(PlaybackStateCompat.Builder())
|
||||
.setActions(ACTIONS)
|
||||
// Active queue ID corresponds to the indices we populated prior, use them here.
|
||||
.setActiveQueueItemId(playbackManager.queue.index.toLong())
|
||||
.setActiveQueueItemId(queue.index.toLong())
|
||||
|
||||
// Android 13+ relies on custom actions in the notification.
|
||||
|
||||
|
@ -399,7 +402,7 @@ constructor(
|
|||
PlaybackStateCompat.CustomAction.Builder(
|
||||
PlaybackService.ACTION_INVERT_SHUFFLE,
|
||||
context.getString(R.string.desc_shuffle),
|
||||
if (playbackManager.queue.isShuffled) {
|
||||
if (playbackManager.isShuffled) {
|
||||
R.drawable.ic_shuffle_on_24
|
||||
} else {
|
||||
R.drawable.ic_shuffle_off_24
|
||||
|
@ -435,7 +438,7 @@ constructor(
|
|||
when (playbackSettings.notificationAction) {
|
||||
ActionMode.SHUFFLE -> {
|
||||
logD("Using shuffle notification action")
|
||||
notification.updateShuffled(playbackManager.queue.isShuffled)
|
||||
notification.updateShuffled(playbackManager.isShuffled)
|
||||
}
|
||||
else -> {
|
||||
logD("Using repeat mode notification action")
|
||||
|
|
|
@ -48,13 +48,20 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.list.ListSettings
|
||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.music.Song
|
||||
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.InternalPlayer
|
||||
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.service.ForegroundManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -82,7 +89,7 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
|||
class PlaybackService :
|
||||
Service(),
|
||||
Player.Listener,
|
||||
InternalPlayer,
|
||||
PlaybackStateHolder,
|
||||
MediaSessionComponent.Listener,
|
||||
MusicRepository.UpdateListener {
|
||||
// Player components
|
||||
|
@ -148,7 +155,7 @@ class PlaybackService :
|
|||
foregroundManager = ForegroundManager(this)
|
||||
// Initialize any listener-dependent components last as we wouldn't want a listener race
|
||||
// condition to cause us to load music before we were fully initialize.
|
||||
playbackManager.registerInternalPlayer(this)
|
||||
playbackManager.registerStateHolder(this)
|
||||
musicRepository.addUpdateListener(this)
|
||||
mediaSessionComponent.registerListener(this)
|
||||
|
||||
|
@ -189,8 +196,8 @@ class PlaybackService :
|
|||
foregroundManager.release()
|
||||
|
||||
// Pause just in case this destruction was unexpected.
|
||||
playbackManager.setPlaying(false)
|
||||
playbackManager.unregisterInternalPlayer(this)
|
||||
playbackManager.playing(false)
|
||||
playbackManager.unregisterStateHolder(this)
|
||||
musicRepository.removeUpdateListener(this)
|
||||
|
||||
unregisterReceiver(systemReceiver)
|
||||
|
@ -210,56 +217,183 @@ class PlaybackService :
|
|||
logD("Service destroyed")
|
||||
}
|
||||
|
||||
// --- CONTROLLER OVERRIDES ---
|
||||
// --- PLAYBACKSTATEHOLDER OVERRIDES ---
|
||||
|
||||
override val audioSessionId: Int
|
||||
get() = player.audioSessionId
|
||||
override val currentSong
|
||||
get() = player.song
|
||||
|
||||
override val shouldRewindWithPrev: Boolean
|
||||
get() = playbackSettings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
||||
override val repeatMode
|
||||
get() = player.repeat
|
||||
|
||||
override fun getState(durationMs: Long) =
|
||||
InternalPlayer.State.from(
|
||||
override val progression: Progression
|
||||
get() =
|
||||
player.song?.let {
|
||||
Progression.from(
|
||||
player.playWhenReady,
|
||||
player.isPlaying,
|
||||
// The position value can be below zero or past the expected duration, make
|
||||
// sure we handle that.
|
||||
player.currentPosition.coerceAtLeast(0).coerceAtMost(durationMs))
|
||||
|
||||
override fun loadSong(song: Song?, play: Boolean) {
|
||||
if (song == null) {
|
||||
// No song, stop playback and foreground state.
|
||||
logD("Nothing playing, stopping playback")
|
||||
// For some reason the player does not mark playWhenReady as false when stopped,
|
||||
// which then completely breaks any re-initialization if playback starts again.
|
||||
// So we manually set it to false here.
|
||||
player.playWhenReady = false
|
||||
player.stop()
|
||||
stopAndSave()
|
||||
return
|
||||
player.currentPosition.coerceAtLeast(0).coerceAtMost(it.durationMs))
|
||||
}
|
||||
?: Progression.nil()
|
||||
|
||||
logD("Loading $song")
|
||||
player.setMediaItem(MediaItem.fromUri(song.uri))
|
||||
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 newPlayback(
|
||||
queue: List<Song>,
|
||||
start: Song?,
|
||||
parent: MusicParent?,
|
||||
shuffled: Boolean,
|
||||
play: Boolean
|
||||
) {
|
||||
this.parent = parent
|
||||
if (shuffled) {
|
||||
player.shuffledQueue(queue, start)
|
||||
} else {
|
||||
player.orderedQueue(queue, start)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
override fun playing(playing: Boolean) {
|
||||
player.playWhenReady = playing
|
||||
}
|
||||
|
||||
override fun repeatMode(repeatMode: RepeatMode) {
|
||||
player.repeatMode =
|
||||
when (repeatMode) {
|
||||
RepeatMode.NONE -> Player.REPEAT_MODE_OFF
|
||||
RepeatMode.ALL -> Player.REPEAT_MODE_ALL
|
||||
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekTo(positionMs: Long) {
|
||||
logD("Seeking to ${positionMs}ms")
|
||||
player.seekTo(positionMs)
|
||||
}
|
||||
|
||||
override fun setPlaying(isPlaying: Boolean) {
|
||||
logD("Updating player state to $isPlaying")
|
||||
player.playWhenReady = isPlaying
|
||||
override fun next() {
|
||||
player.seekToNext()
|
||||
player.play()
|
||||
}
|
||||
|
||||
override fun prev() {
|
||||
player.seekToPrevious()
|
||||
player.play()
|
||||
}
|
||||
|
||||
override fun goto(index: Int) {
|
||||
player.goto(index)
|
||||
player.play()
|
||||
}
|
||||
|
||||
override fun reorder(shuffled: Boolean) {
|
||||
player.reorder(shuffled)
|
||||
playbackManager.dispatchEvent(
|
||||
this,
|
||||
PlaybackEvent.QueueReordered(
|
||||
Queue(
|
||||
player.currentIndex,
|
||||
player.resolveQueue(),
|
||||
),
|
||||
shuffled))
|
||||
}
|
||||
|
||||
override fun addToQueue(songs: List<Song>) {
|
||||
val insertAt = player.currentIndex + 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))))
|
||||
}
|
||||
|
||||
override fun playNext(songs: List<Song>) {
|
||||
val insertAt = player.currentIndex + 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))))
|
||||
}
|
||||
|
||||
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.Diff)))
|
||||
}
|
||||
|
||||
override fun remove(at: Int) {
|
||||
val oldUnscrambledIndex = player.currentIndex
|
||||
val oldScrambledIndex = 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
|
||||
}
|
||||
|
||||
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.Diff)))
|
||||
}
|
||||
|
||||
// --- PLAYER OVERRIDES ---
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
super.onEvents(player, events)
|
||||
if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
||||
|
||||
if (player.playWhenReady) {
|
||||
// Mark that we have started playing so that the notification can now be posted.
|
||||
hasPlayed = true
|
||||
|
@ -279,32 +413,25 @@ class PlaybackService :
|
|||
}
|
||||
}
|
||||
|
||||
// Any change to the analogous isPlaying, isAdvancing, or positionMs values require
|
||||
// us to synchronize with a new state.
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
super.onMediaItemTransition(mediaItem, reason)
|
||||
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
|
||||
reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) {
|
||||
playbackManager.dispatchEvent(
|
||||
this, PlaybackEvent.IndexMoved(player.song, player.currentIndex))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
super.onEvents(player, events)
|
||||
|
||||
if (events.containsAny(
|
||||
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
||||
Player.EVENT_IS_PLAYING_CHANGED,
|
||||
Player.EVENT_POSITION_DISCONTINUITY)) {
|
||||
logD("Player state changed, must synchronize state")
|
||||
playbackManager.synchronizeState(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(state: Int) {
|
||||
if (state == Player.STATE_ENDED) {
|
||||
// Player ended, repeat the current track if we are configured to.
|
||||
if (playbackManager.repeatMode == RepeatMode.TRACK) {
|
||||
logD("Looping current track")
|
||||
playbackManager.rewind()
|
||||
// May be configured to pause when we repeat a track.
|
||||
if (playbackSettings.pauseOnRepeat) {
|
||||
logD("Pausing track on loop")
|
||||
playbackManager.setPlaying(false)
|
||||
}
|
||||
} else {
|
||||
logD("Track ended, moving to next track")
|
||||
playbackManager.next()
|
||||
}
|
||||
playbackManager.dispatchEvent(this, PlaybackEvent.ProgressionChanged(progression))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -347,7 +474,7 @@ class PlaybackService :
|
|||
}
|
||||
}
|
||||
|
||||
override fun performAction(action: InternalPlayer.Action): Boolean {
|
||||
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
||||
val deviceLibrary =
|
||||
musicRepository.deviceLibrary
|
||||
// No library, cannot do anything.
|
||||
|
@ -355,7 +482,7 @@ class PlaybackService :
|
|||
|
||||
when (action) {
|
||||
// Restore state -> Start a new restoreState job
|
||||
is InternalPlayer.Action.RestoreState -> {
|
||||
is DeferredPlayback.RestoreState -> {
|
||||
logD("Restoring playback state")
|
||||
restoreScope.launch {
|
||||
persistenceRepository.readState()?.let {
|
||||
|
@ -366,20 +493,20 @@ class PlaybackService :
|
|||
}
|
||||
}
|
||||
// Shuffle all -> Start new playback from all songs
|
||||
is InternalPlayer.Action.ShuffleAll -> {
|
||||
is DeferredPlayback.ShuffleAll -> {
|
||||
logD("Shuffling all tracks")
|
||||
playbackManager.play(
|
||||
null, null, listSettings.songSort.songs(deviceLibrary.songs), true)
|
||||
}
|
||||
// Open -> Try to find the Song for the given file and then play it from all songs
|
||||
is InternalPlayer.Action.Open -> {
|
||||
is DeferredPlayback.Open -> {
|
||||
logD("Opening specified file")
|
||||
deviceLibrary.findSongForUri(application, action.uri)?.let { song ->
|
||||
playbackManager.play(
|
||||
song,
|
||||
null,
|
||||
listSettings.songSort.songs(deviceLibrary.songs),
|
||||
playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
|
||||
player.shuffleModeEnabled && playbackSettings.keepShuffle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -437,15 +564,15 @@ class PlaybackService :
|
|||
// --- AUXIO EVENTS ---
|
||||
ACTION_PLAY_PAUSE -> {
|
||||
logD("Received play event")
|
||||
playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
|
||||
playbackManager.playing(!playbackManager.progression.isPlaying)
|
||||
}
|
||||
ACTION_INC_REPEAT_MODE -> {
|
||||
logD("Received repeat mode event")
|
||||
playbackManager.repeatMode = playbackManager.repeatMode.increment()
|
||||
playbackManager.repeatMode(playbackManager.repeatMode.increment())
|
||||
}
|
||||
ACTION_INVERT_SHUFFLE -> {
|
||||
logD("Received shuffle event")
|
||||
playbackManager.reorder(!playbackManager.queue.isShuffled)
|
||||
playbackManager.reorder(!playbackManager.isShuffled)
|
||||
}
|
||||
ACTION_SKIP_PREV -> {
|
||||
logD("Received skip previous event")
|
||||
|
@ -457,7 +584,7 @@ class PlaybackService :
|
|||
}
|
||||
ACTION_EXIT -> {
|
||||
logD("Received exit event")
|
||||
playbackManager.setPlaying(false)
|
||||
playbackManager.playing(false)
|
||||
stopAndSave()
|
||||
}
|
||||
WidgetProvider.ACTION_WIDGET_UPDATE -> {
|
||||
|
@ -472,17 +599,17 @@ 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 (playbackSettings.headsetAutoplay &&
|
||||
playbackManager.queue.currentSong != null &&
|
||||
playbackManager.currentSong != null &&
|
||||
initialHeadsetPlugEventHandled) {
|
||||
logD("Device connected, resuming")
|
||||
playbackManager.setPlaying(true)
|
||||
playbackManager.playing(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pauseFromHeadsetPlug() {
|
||||
if (playbackManager.queue.currentSong != null) {
|
||||
if (playbackManager.currentSong != null) {
|
||||
logD("Device disconnected, pausing")
|
||||
playbackManager.setPlaying(false)
|
||||
playbackManager.playing(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,11 +29,11 @@ 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.InternalPlayer
|
||||
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.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
import org.oxycblt.auxio.util.getDimenPixels
|
||||
|
@ -64,7 +64,7 @@ constructor(
|
|||
|
||||
/** Update [WidgetProvider] with the current playback state. */
|
||||
fun update() {
|
||||
val song = playbackManager.queue.currentSong
|
||||
val song = playbackManager.currentSong
|
||||
if (song == null) {
|
||||
logD("No song, resetting widget")
|
||||
widgetProvider.update(context, uiSettings, null)
|
||||
|
@ -72,9 +72,9 @@ constructor(
|
|||
}
|
||||
|
||||
// Note: Store these values here so they remain consistent once the bitmap is loaded.
|
||||
val isPlaying = playbackManager.playerState.isPlaying
|
||||
val isPlaying = playbackManager.progression.isPlaying
|
||||
val repeatMode = playbackManager.repeatMode
|
||||
val isShuffled = playbackManager.queue.isShuffled
|
||||
val isShuffled = playbackManager.isShuffled
|
||||
|
||||
logD("Updating widget with new playback state")
|
||||
bitmapProvider.load(
|
||||
|
@ -135,16 +135,16 @@ constructor(
|
|||
|
||||
// --- CALLBACKS ---
|
||||
|
||||
// Respond to all major song or player changes that will affect the widget
|
||||
override fun onIndexMoved(queue: Queue) = update()
|
||||
|
||||
override fun onQueueReordered(queue: Queue) = update()
|
||||
|
||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) = update()
|
||||
|
||||
override fun onStateChanged(state: InternalPlayer.State) = update()
|
||||
|
||||
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
|
||||
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) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
// Respond to settings changes that will affect the widget
|
||||
override fun onRoundModeChanged() = update()
|
||||
|
@ -156,7 +156,7 @@ constructor(
|
|||
*
|
||||
* @param song [Queue.currentSong]
|
||||
* @param cover A pre-loaded album cover [Bitmap] for [song].
|
||||
* @param isPlaying [PlaybackStateManager.playerState]
|
||||
* @param isPlaying [PlaybackStateManager.progression]
|
||||
* @param repeatMode [PlaybackStateManager.repeatMode]
|
||||
* @param isShuffled [Queue.isShuffled]
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue