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.databinding.ActivityMainBinding
|
||||||
import org.oxycblt.auxio.music.system.IndexerService
|
import org.oxycblt.auxio.music.system.IndexerService
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
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.playback.system.PlaybackService
|
||||||
import org.oxycblt.auxio.ui.UISettings
|
import org.oxycblt.auxio.ui.UISettings
|
||||||
import org.oxycblt.auxio.util.isNight
|
import org.oxycblt.auxio.util.isNight
|
||||||
|
@ -76,7 +76,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
if (!startIntentAction(intent)) {
|
if (!startIntentAction(intent)) {
|
||||||
// No intent action to do, just restore the previously saved state.
|
// 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 =
|
val action =
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
|
Intent.ACTION_VIEW -> DeferredPlayback.Open(intent.data ?: return false)
|
||||||
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
|
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> DeferredPlayback.ShuffleAll
|
||||||
else -> {
|
else -> {
|
||||||
logW("Unexpected intent ${intent.action}")
|
logW("Unexpected intent ${intent.action}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logD("Translated intent to $action")
|
logD("Translated intent to $action")
|
||||||
playbackModel.startAction(action)
|
playbackModel.playDeferred(action)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,6 @@ import kotlinx.coroutines.Job
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.IndexingProgress
|
import org.oxycblt.auxio.music.IndexingProgress
|
||||||
import org.oxycblt.auxio.music.IndexingState
|
import org.oxycblt.auxio.music.IndexingState
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.fs.contentResolverSafe
|
import org.oxycblt.auxio.music.fs.contentResolverSafe
|
||||||
|
@ -139,22 +138,23 @@ class IndexerService :
|
||||||
logD("Music changed, updating shared objects")
|
logD("Music changed, updating shared objects")
|
||||||
// Wipe possibly-invalidated outdated covers
|
// Wipe possibly-invalidated outdated covers
|
||||||
imageLoader.memoryCache?.clear()
|
imageLoader.memoryCache?.clear()
|
||||||
// Clear invalid models from PlaybackStateManager. This is not connected
|
// // Clear invalid models from PlaybackStateManager. This is not connected
|
||||||
// to a listener as it is bad practice for a shared object to attach to
|
// // to a listener as it is bad practice for a shared object to attach to
|
||||||
// the listener system of another.
|
// // the listener system of another.
|
||||||
playbackManager.toSavedState()?.let { savedState ->
|
// playbackManager.toSavedState()?.let { savedState ->
|
||||||
playbackManager.applySavedState(
|
// playbackManager.applySavedState(
|
||||||
PlaybackStateManager.SavedState(
|
// PlaybackStateManager.SavedState(
|
||||||
parent =
|
// parent =
|
||||||
savedState.parent?.let { musicRepository.find(it.uid) as? MusicParent },
|
// savedState.parent?.let { musicRepository.find(it.uid) as?
|
||||||
queueState =
|
// MusicParent },
|
||||||
savedState.queueState.remap { song ->
|
// queueState =
|
||||||
deviceLibrary.findSong(requireNotNull(song).uid)
|
// savedState.queueState.remap { song ->
|
||||||
},
|
// deviceLibrary.findSong(requireNotNull(song).uid)
|
||||||
positionMs = savedState.positionMs,
|
// },
|
||||||
repeatMode = savedState.repeatMode),
|
// positionMs = savedState.positionMs,
|
||||||
true)
|
// repeatMode = savedState.repeatMode),
|
||||||
}
|
// true)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexingStateChanged() {
|
override fun onIndexingStateChanged() {
|
||||||
|
|
|
@ -36,9 +36,10 @@ import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||||
import org.oxycblt.auxio.playback.queue.Queue
|
import org.oxycblt.auxio.playback.state.DeferredPlayback
|
||||||
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.PlaybackStateManager
|
||||||
|
import org.oxycblt.auxio.playback.state.QueueChange
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.util.Event
|
import org.oxycblt.auxio.util.Event
|
||||||
import org.oxycblt.auxio.util.MutableEvent
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
|
@ -129,51 +130,52 @@ constructor(
|
||||||
playbackSettings.unregisterListener(this)
|
playbackSettings.unregisterListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexMoved(queue: Queue) {
|
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||||
logD("Index moved, updating current song")
|
when (event) {
|
||||||
_song.value = queue.currentSong
|
is PlaybackEvent.IndexMoved -> {
|
||||||
}
|
logD("Index moved, updating current song")
|
||||||
|
_song.value = event.currentSong
|
||||||
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
|
}
|
||||||
// Other types of queue changes preserve the current song.
|
is PlaybackEvent.QueueChanged -> {
|
||||||
if (change.type == Queue.Change.Type.SONG) {
|
// Other types of queue changes preserve the current song.
|
||||||
logD("Queue changed, updating current song")
|
if (event.change.type == QueueChange.Type.SONG) {
|
||||||
_song.value = queue.currentSong
|
logD("Queue changed, updating current song")
|
||||||
}
|
_song.value = event.currentSong
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueueReordered(queue: Queue) {
|
|
||||||
logD("Queue completely changed, updating current song")
|
|
||||||
_isShuffled.value = queue.isShuffled
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
|
||||||
logD("New playback started, updating playback information")
|
|
||||||
_song.value = queue.currentSong
|
|
||||||
_parent.value = parent
|
|
||||||
_isShuffled.value = queue.isShuffled
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStateChanged(state: InternalPlayer.State) {
|
|
||||||
logD("Player state changed, starting new position polling")
|
|
||||||
_isPlaying.value = state.isPlaying
|
|
||||||
// Still need to update the position now due to co-routine launch delays
|
|
||||||
_positionDs.value = state.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()
|
|
||||||
// Wait a deci-second for the next position tick.
|
|
||||||
delay(100)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
is PlaybackEvent.QueueReordered -> {
|
||||||
|
logD("Queue completely changed, updating current song")
|
||||||
override fun onRepeatChanged(repeatMode: RepeatMode) {
|
_isShuffled.value = event.isShuffled
|
||||||
_repeatMode.value = repeatMode
|
}
|
||||||
|
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 onBarActionChanged() {
|
override fun onBarActionChanged() {
|
||||||
|
@ -223,8 +225,7 @@ constructor(
|
||||||
playFromGenreImpl(song, genre, isImplicitlyShuffled())
|
playFromGenreImpl(song, genre, isImplicitlyShuffled())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isImplicitlyShuffled() =
|
private fun isImplicitlyShuffled() = playbackManager.isShuffled && playbackSettings.keepShuffle
|
||||||
playbackManager.queue.isShuffled && playbackSettings.keepShuffle
|
|
||||||
|
|
||||||
private fun playWithImpl(song: Song, with: PlaySong, shuffled: Boolean) {
|
private fun playWithImpl(song: Song, with: PlaySong, shuffled: Boolean) {
|
||||||
when (with) {
|
when (with) {
|
||||||
|
@ -416,9 +417,9 @@ constructor(
|
||||||
*
|
*
|
||||||
* @param action The [InternalPlayer.Action] to perform eventually.
|
* @param action The [InternalPlayer.Action] to perform eventually.
|
||||||
*/
|
*/
|
||||||
fun startAction(action: InternalPlayer.Action) {
|
fun playDeferred(action: DeferredPlayback) {
|
||||||
logD("Starting action $action")
|
logD("Starting action $action")
|
||||||
playbackManager.startAction(action)
|
playbackManager.playDeferred(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PLAYER FUNCTIONS ---
|
// --- PLAYER FUNCTIONS ---
|
||||||
|
@ -572,13 +573,13 @@ constructor(
|
||||||
/** Toggle [isPlaying] (i.e from playing to paused) */
|
/** Toggle [isPlaying] (i.e from playing to paused) */
|
||||||
fun togglePlaying() {
|
fun togglePlaying() {
|
||||||
logD("Toggling playing state")
|
logD("Toggling playing state")
|
||||||
playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
|
playbackManager.playing(!playbackManager.progression.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Toggle [isShuffled] (ex. from on to off) */
|
/** Toggle [isShuffled] (ex. from on to off) */
|
||||||
fun toggleShuffled() {
|
fun toggleShuffled() {
|
||||||
logD("Toggling shuffled state")
|
logD("Toggling shuffled state")
|
||||||
playbackManager.reorder(!playbackManager.queue.isShuffled)
|
playbackManager.reorder(!playbackManager.isShuffled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -588,7 +589,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun toggleRepeatMode() {
|
fun toggleRepeatMode() {
|
||||||
logD("Toggling repeat mode")
|
logD("Toggling repeat mode")
|
||||||
playbackManager.repeatMode = playbackManager.repeatMode.increment()
|
playbackManager.repeatMode(playbackManager.repeatMode.increment())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- UI CONTROL ---
|
// --- UI CONTROL ---
|
||||||
|
|
|
@ -21,7 +21,6 @@ package org.oxycblt.auxio.playback.persist
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.playback.queue.Queue
|
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
|
@ -76,17 +75,19 @@ constructor(
|
||||||
val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent }
|
val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent }
|
||||||
logD("Successfully read playback state")
|
logD("Successfully read playback state")
|
||||||
|
|
||||||
return PlaybackStateManager.SavedState(
|
// return PlaybackStateManager.SavedState(
|
||||||
parent = parent,
|
// parent = parent,
|
||||||
queueState =
|
// queueState =
|
||||||
Queue.SavedState(
|
// Queue.SavedState(
|
||||||
heap.map { deviceLibrary.findSong(it.uid) },
|
// heap.map { deviceLibrary.findSong(it.uid) },
|
||||||
orderedMapping,
|
// orderedMapping,
|
||||||
shuffledMapping,
|
// shuffledMapping,
|
||||||
playbackState.index,
|
// playbackState.index,
|
||||||
playbackState.songUid),
|
// playbackState.songUid),
|
||||||
positionMs = playbackState.positionMs,
|
// positionMs = playbackState.positionMs,
|
||||||
repeatMode = playbackState.repeatMode)
|
// repeatMode = playbackState.repeatMode)
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean {
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
|
||||||
import org.oxycblt.auxio.music.Song
|
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.PlaybackStateManager
|
||||||
|
import org.oxycblt.auxio.playback.state.QueueChange
|
||||||
import org.oxycblt.auxio.util.Event
|
import org.oxycblt.auxio.util.Event
|
||||||
import org.oxycblt.auxio.util.MutableEvent
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -51,7 +52,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
|
||||||
val scrollTo: Event<Int>
|
val scrollTo: Event<Int>
|
||||||
get() = _scrollTo
|
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. */
|
/** The index of the currently playing song in the queue. */
|
||||||
val index: StateFlow<Int>
|
val index: StateFlow<Int>
|
||||||
get() = _index
|
get() = _index
|
||||||
|
@ -60,42 +61,45 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
|
||||||
playbackManager.addListener(this)
|
playbackManager.addListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexMoved(queue: Queue) {
|
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||||
logD("Index moved, synchronizing and scrolling to new position")
|
when (event) {
|
||||||
_scrollTo.put(queue.index)
|
is PlaybackEvent.IndexMoved -> {
|
||||||
_index.value = queue.index
|
logD("Index moved, synchronizing and scrolling to new position")
|
||||||
}
|
_scrollTo.put(event.index)
|
||||||
|
_index.value = event.index
|
||||||
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
|
}
|
||||||
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
|
is PlaybackEvent.QueueChanged -> {
|
||||||
logD("Updating queue display")
|
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
|
||||||
_queueInstructions.put(change.instructions)
|
logD("Updating queue display")
|
||||||
_queue.value = queue.resolve()
|
_queueInstructions.put(event.change.instructions)
|
||||||
if (change.type != Queue.Change.Type.MAPPING) {
|
_queue.value = event.queue.queue
|
||||||
// Index changed, make sure it remains updated without actually scrolling to it.
|
if (event.change.type != QueueChange.Type.MAPPING) {
|
||||||
logD("Index changed with queue, synchronizing new position")
|
// Index changed, make sure it remains updated without actually scrolling to it.
|
||||||
_index.value = queue.index
|
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 onQueueReordered(queue: Queue) {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
playbackManager.removeListener(this)
|
playbackManager.removeListener(this)
|
||||||
|
|
|
@ -27,11 +27,11 @@ import java.nio.ByteBuffer
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
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.PlaybackStateManager
|
||||||
|
import org.oxycblt.auxio.playback.state.QueueChange
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -70,26 +70,35 @@ constructor(
|
||||||
|
|
||||||
// --- OVERRIDES ---
|
// --- OVERRIDES ---
|
||||||
|
|
||||||
override fun onIndexMoved(queue: Queue) {
|
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||||
logD("Index moved, updating current song")
|
when (event) {
|
||||||
applyReplayGain(queue.currentSong)
|
is PlaybackEvent.IndexMoved -> {
|
||||||
}
|
logD("Index moved, updating current song")
|
||||||
|
applyReplayGain(event.currentSong)
|
||||||
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
|
}
|
||||||
// Other types of queue changes preserve the current song.
|
is PlaybackEvent.QueueChanged -> {
|
||||||
if (change.type == Queue.Change.Type.SONG) {
|
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
|
||||||
applyReplayGain(queue.currentSong)
|
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 onNewPlayback(queue: Queue, parent: MusicParent?) {
|
|
||||||
logD("New playback started, updating playback information")
|
|
||||||
applyReplayGain(queue.currentSong)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReplayGainSettingsChanged() {
|
override fun onReplayGainSettingsChanged() {
|
||||||
// ReplayGain config changed, we need to set it up again.
|
// ReplayGain config changed, we need to set it up again.
|
||||||
applyReplayGain(playbackManager.queue.currentSong)
|
applyReplayGain(playbackManager.currentSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- REPLAYGAIN PARSING ---
|
// --- REPLAYGAIN PARSING ---
|
||||||
|
@ -131,7 +140,7 @@ constructor(
|
||||||
logD("Using dynamic strategy")
|
logD("Using dynamic strategy")
|
||||||
gain.album?.takeIf {
|
gain.album?.takeIf {
|
||||||
playbackManager.parent is Album &&
|
playbackManager.parent is Album &&
|
||||||
playbackManager.queue.currentSong?.album == playbackManager.parent
|
playbackManager.currentSong?.album == playbackManager.parent
|
||||||
}
|
}
|
||||||
?: gain.track
|
?: 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.BuildConfig
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.queue.MutableQueue
|
|
||||||
import org.oxycblt.auxio.playback.queue.Queue
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
|
@ -40,19 +38,26 @@ import org.oxycblt.auxio.util.logW
|
||||||
* PlaybackService.
|
* PlaybackService.
|
||||||
*
|
*
|
||||||
* Internal consumers should usually use [Listener], however the component that manages the player
|
* 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)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
interface PlaybackStateManager {
|
interface PlaybackStateManager {
|
||||||
/** The current [Queue]. */
|
/** The current [Progression] state. */
|
||||||
val queue: Queue
|
val progression: Progression
|
||||||
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
|
||||||
|
val currentSong: Song?
|
||||||
|
|
||||||
|
val repeatMode: RepeatMode
|
||||||
|
|
||||||
|
val audioSessionId: Int
|
||||||
|
|
||||||
val parent: MusicParent?
|
val parent: MusicParent?
|
||||||
/** The current [InternalPlayer] state. */
|
|
||||||
val playerState: InternalPlayer.State
|
val isShuffled: Boolean
|
||||||
/** The current [RepeatMode] */
|
|
||||||
var repeatMode: RepeatMode
|
fun resolveQueue(): Queue
|
||||||
|
|
||||||
/** The audio session ID of the internal player. Null if no internal player exists. */
|
/** The audio session ID of the internal player. Null if no internal player exists. */
|
||||||
val currentAudioSessionId: Int?
|
val currentAudioSessionId: Int?
|
||||||
|
|
||||||
|
@ -75,23 +80,25 @@ interface PlaybackStateManager {
|
||||||
fun removeListener(listener: Listener)
|
fun removeListener(listener: Listener)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an [InternalPlayer] for this instance. This instance will handle translating the
|
* Register an [PlaybackStateHolder] for this instance. This instance will handle translating
|
||||||
* current playback state into audio playback. There can be only one [InternalPlayer] at a time.
|
* the current playback state into audio playback. There can be only one [PlaybackStateHolder]
|
||||||
* Will invoke [InternalPlayer] methods to initialize the instance with the current state.
|
* 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.
|
* registered.
|
||||||
*/
|
*/
|
||||||
fun registerInternalPlayer(internalPlayer: InternalPlayer)
|
fun registerStateHolder(stateHolder: PlaybackStateHolder)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister the [InternalPlayer] from this instance, prevent it from receiving any further
|
* Unregister the [PlaybackStateHolder] from this instance, prevent it from receiving any
|
||||||
* commands.
|
* further commands.
|
||||||
*
|
*
|
||||||
* @param internalPlayer The [InternalPlayer] to unregister. Must be the current
|
* @param stateHolder The [PlaybackStateHolder] to unregister. Must be the current
|
||||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
* [PlaybackStateHolder]. Does nothing if invoked by another [PlaybackStateHolder]
|
||||||
|
* implementation.
|
||||||
*/
|
*/
|
||||||
fun unregisterInternalPlayer(internalPlayer: InternalPlayer)
|
fun unregisterStateHolder(stateHolder: PlaybackStateHolder)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start new playback.
|
* Start new playback.
|
||||||
|
@ -173,36 +180,33 @@ interface PlaybackStateManager {
|
||||||
*/
|
*/
|
||||||
fun reorder(shuffled: Boolean)
|
fun reorder(shuffled: Boolean)
|
||||||
|
|
||||||
/**
|
fun dispatchEvent(stateHolder: PlaybackStateHolder, vararg events: PlaybackEvent)
|
||||||
* 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)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* Request that the pending [PlaybackStateHolder.Action] (if any) be passed to the given
|
||||||
* [InternalPlayer].
|
* [PlaybackStateHolder].
|
||||||
*
|
*
|
||||||
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
|
* @param stateHolder The [PlaybackStateHolder] to synchronize with. Must be the current
|
||||||
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
|
* [PlaybackStateHolder]. Does nothing if invoked by another [PlaybackStateHolder]
|
||||||
|
* implementation.
|
||||||
*/
|
*/
|
||||||
fun requestAction(internalPlayer: InternalPlayer)
|
fun requestAction(stateHolder: PlaybackStateHolder)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update whether playback is ongoing or not.
|
* Update whether playback is ongoing or not.
|
||||||
*
|
*
|
||||||
* @param isPlaying 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].
|
* 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].
|
* [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener].
|
||||||
*/
|
*/
|
||||||
interface Listener {
|
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.
|
|
||||||
*
|
|
||||||
* @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) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A condensed representation of the playback state that can be persisted.
|
* A condensed representation of the playback state that can be persisted.
|
||||||
*
|
*
|
||||||
* @param parent The [MusicParent] item currently being played from.
|
* @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 positionMs The current position in the currently played song, in ms
|
||||||
* @param repeatMode The current [RepeatMode].
|
* @param repeatMode The current [RepeatMode].
|
||||||
*/
|
*/
|
||||||
data class SavedState(
|
data class SavedState(
|
||||||
val parent: MusicParent?,
|
val parent: MusicParent?,
|
||||||
val queueState: Queue.SavedState,
|
val queueState: SavedQueue,
|
||||||
val positionMs: Long,
|
val positionMs: Long,
|
||||||
val repeatMode: RepeatMode,
|
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 {
|
class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
||||||
private val listeners = mutableListOf<PlaybackStateManager.Listener>()
|
private val listeners = mutableListOf<PlaybackStateManager.Listener>()
|
||||||
@Volatile private var internalPlayer: InternalPlayer? = null
|
@Volatile private var stateHolder: PlaybackStateHolder? = null
|
||||||
@Volatile private var pendingAction: InternalPlayer.Action? = null
|
@Volatile private var pendingDeferredPlayback: DeferredPlayback? = null
|
||||||
@Volatile private var isInitialized = false
|
@Volatile private var isInitialized = false
|
||||||
|
|
||||||
override val queue = MutableQueue()
|
/** The current [Progression] state. */
|
||||||
@Volatile
|
override val progression: Progression
|
||||||
override var parent: MusicParent? = null
|
get() = stateHolder?.progression ?: Progression.nil()
|
||||||
private set
|
|
||||||
|
|
||||||
@Volatile
|
override val currentSong: Song?
|
||||||
override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
|
get() = stateHolder?.currentSong
|
||||||
private set
|
|
||||||
|
|
||||||
@Volatile
|
override val repeatMode: RepeatMode
|
||||||
override var repeatMode = RepeatMode.NONE
|
get() = stateHolder?.repeatMode ?: RepeatMode.NONE
|
||||||
set(value) {
|
|
||||||
field = value
|
override val audioSessionId: Int
|
||||||
notifyRepeatModeChanged()
|
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?
|
override val currentAudioSessionId: Int?
|
||||||
get() = internalPlayer?.audioSessionId
|
get() = stateHolder?.audioSessionId
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun addListener(listener: PlaybackStateManager.Listener) {
|
override fun addListener(listener: PlaybackStateManager.Listener) {
|
||||||
logD("Adding $listener to listeners")
|
logD("Adding $listener to listeners")
|
||||||
if (isInitialized) {
|
|
||||||
listener.onNewPlayback(queue, parent)
|
|
||||||
listener.onRepeatChanged(repeatMode)
|
|
||||||
listener.onStateChanged(playerState)
|
|
||||||
}
|
|
||||||
|
|
||||||
listeners.add(listener)
|
listeners.add(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,286 +324,211 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun registerInternalPlayer(internalPlayer: InternalPlayer) {
|
override fun registerStateHolder(stateHolder: PlaybackStateHolder) {
|
||||||
if (this.internalPlayer != null) {
|
if (this.stateHolder != null) {
|
||||||
logW("Internal player is already registered")
|
logW("Internal player is already registered")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Registering internal player $internalPlayer")
|
this.stateHolder = stateHolder
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun unregisterInternalPlayer(internalPlayer: InternalPlayer) {
|
override fun unregisterStateHolder(stateHolder: PlaybackStateHolder) {
|
||||||
if (this.internalPlayer !== internalPlayer) {
|
if (this.stateHolder !== stateHolder) {
|
||||||
logW("Given internal player did not match current internal player")
|
logW("Given internal player did not match current internal player")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Unregistering internal player $internalPlayer")
|
logD("Unregistering internal player $stateHolder")
|
||||||
|
|
||||||
this.internalPlayer = null
|
this.stateHolder = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PLAYING FUNCTIONS ---
|
// --- PLAYING FUNCTIONS ---
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean) {
|
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]")
|
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
|
// Played something, so we are initialized now
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
|
stateHolder.newPlayback(queue, song, parent, shuffled, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- QUEUE FUNCTIONS ---
|
// --- QUEUE FUNCTIONS ---
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun next() {
|
override fun next() {
|
||||||
val internalPlayer = internalPlayer ?: return
|
val stateHolder = stateHolder ?: return
|
||||||
var play = true
|
logD("Going to next song")
|
||||||
if (!queue.goto(queue.index + 1)) {
|
stateHolder.next()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun prev() {
|
override fun prev() {
|
||||||
val internalPlayer = internalPlayer ?: return
|
val stateHolder = stateHolder ?: return
|
||||||
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
|
logD("Going to previous song")
|
||||||
if (internalPlayer.shouldRewindWithPrev) {
|
stateHolder.prev()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun goto(index: Int) {
|
override fun goto(index: Int) {
|
||||||
val internalPlayer = internalPlayer ?: return
|
val stateHolder = stateHolder ?: return
|
||||||
if (queue.goto(index)) {
|
logD("Going to index $index")
|
||||||
logD("Moving to $index")
|
stateHolder.goto(index)
|
||||||
notifyIndexMoved()
|
|
||||||
internalPlayer.loadSong(queue.currentSong, true)
|
|
||||||
} else {
|
|
||||||
logW("$index was not in bounds, could not move to it")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun playNext(songs: List<Song>) {
|
override fun playNext(songs: List<Song>) {
|
||||||
if (queue.currentSong == null) {
|
if (currentSong == null) {
|
||||||
logD("Nothing playing, short-circuiting to new playback")
|
logD("Nothing playing, short-circuiting to new playback")
|
||||||
play(songs[0], null, songs, false)
|
play(songs[0], null, songs, false)
|
||||||
} else {
|
} else {
|
||||||
|
val stateHolder = stateHolder ?: return
|
||||||
logD("Adding ${songs.size} songs to start of queue")
|
logD("Adding ${songs.size} songs to start of queue")
|
||||||
notifyQueueChanged(queue.addToTop(songs))
|
stateHolder.playNext(songs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun addToQueue(songs: List<Song>) {
|
override fun addToQueue(songs: List<Song>) {
|
||||||
if (queue.currentSong == null) {
|
if (currentSong == null) {
|
||||||
logD("Nothing playing, short-circuiting to new playback")
|
logD("Nothing playing, short-circuiting to new playback")
|
||||||
play(songs[0], null, songs, false)
|
play(songs[0], null, songs, false)
|
||||||
} else {
|
} else {
|
||||||
|
val stateHolder = stateHolder ?: return
|
||||||
logD("Adding ${songs.size} songs to end of queue")
|
logD("Adding ${songs.size} songs to end of queue")
|
||||||
notifyQueueChanged(queue.addToBottom(songs))
|
stateHolder.addToQueue(songs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun moveQueueItem(src: Int, dst: Int) {
|
override fun moveQueueItem(src: Int, dst: Int) {
|
||||||
|
val stateHolder = stateHolder ?: return
|
||||||
logD("Moving item $src to position $dst")
|
logD("Moving item $src to position $dst")
|
||||||
notifyQueueChanged(queue.move(src, dst))
|
stateHolder.move(src, dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun removeQueueItem(at: Int) {
|
override fun removeQueueItem(at: Int) {
|
||||||
val internalPlayer = internalPlayer ?: return
|
val stateHolder = stateHolder ?: return
|
||||||
logD("Removing item at $at")
|
logD("Removing item at $at")
|
||||||
val change = queue.remove(at)
|
stateHolder.remove(at)
|
||||||
if (change.type == Queue.Change.Type.SONG) {
|
|
||||||
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
|
|
||||||
}
|
|
||||||
notifyQueueChanged(change)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun reorder(shuffled: Boolean) {
|
override fun reorder(shuffled: Boolean) {
|
||||||
|
val stateHolder = stateHolder ?: return
|
||||||
logD("Reordering queue [shuffled=$shuffled]")
|
logD("Reordering queue [shuffled=$shuffled]")
|
||||||
queue.reorder(shuffled)
|
stateHolder.reorder(shuffled)
|
||||||
notifyQueueReordered()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- INTERNAL PLAYER FUNCTIONS ---
|
// --- INTERNAL PLAYER FUNCTIONS ---
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun synchronizeState(internalPlayer: InternalPlayer) {
|
override fun playDeferred(action: DeferredPlayback) {
|
||||||
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
|
val stateHolder = stateHolder
|
||||||
logW("Given internal player did not match current internal player")
|
if (stateHolder == null || !stateHolder.handleDeferred(action)) {
|
||||||
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)) {
|
|
||||||
logD("Internal player not present or did not consume action, waiting")
|
logD("Internal player not present or did not consume action, waiting")
|
||||||
pendingAction = action
|
pendingDeferredPlayback = action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun requestAction(internalPlayer: InternalPlayer) {
|
override fun requestAction(stateHolder: PlaybackStateHolder) {
|
||||||
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
|
if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) {
|
||||||
logW("Given internal player did not match current internal player")
|
logW("Given internal player did not match current internal player")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingAction?.let(internalPlayer::performAction) == true) {
|
if (pendingDeferredPlayback?.let(stateHolder::handleDeferred) == true) {
|
||||||
logD("Pending action consumed")
|
logD("Pending action consumed")
|
||||||
pendingAction = null
|
pendingDeferredPlayback = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun setPlaying(isPlaying: Boolean) {
|
override fun playing(isPlaying: Boolean) {
|
||||||
|
val stateHolder = stateHolder ?: return
|
||||||
logD("Updating playing state to $isPlaying")
|
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
|
@Synchronized
|
||||||
override fun seekTo(positionMs: Long) {
|
override fun seekTo(positionMs: Long) {
|
||||||
|
val stateHolder = stateHolder ?: return
|
||||||
logD("Seeking to ${positionMs}ms")
|
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 ---
|
// --- PERSISTENCE FUNCTIONS ---
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized override fun toSavedState() = null
|
||||||
override fun toSavedState() =
|
// queue.toSavedState()?.let {
|
||||||
queue.toSavedState()?.let {
|
// PlaybackStateManager.SavedState(
|
||||||
PlaybackStateManager.SavedState(
|
// parent = parent,
|
||||||
parent = parent,
|
// queueState = it,
|
||||||
queueState = it,
|
// positionMs = progression.calculateElapsedPositionMs(),
|
||||||
positionMs = playerState.calculateElapsedPositionMs(),
|
// repeatMode = repeatMode)
|
||||||
repeatMode = repeatMode)
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun applySavedState(
|
override fun applySavedState(
|
||||||
savedState: PlaybackStateManager.SavedState,
|
savedState: PlaybackStateManager.SavedState,
|
||||||
destructive: Boolean
|
destructive: Boolean
|
||||||
) {
|
) {
|
||||||
if (isInitialized && !destructive) {
|
// if (isInitialized && !destructive) {
|
||||||
logW("Already initialized, cannot apply saved state")
|
// logW("Already initialized, cannot apply saved state")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
val internalPlayer = internalPlayer ?: return
|
// val stateHolder = stateHolder ?: return
|
||||||
logD("Applying state $savedState")
|
// logD("Applying state $savedState")
|
||||||
|
//
|
||||||
val lastSong = queue.currentSong
|
// val lastSong = queue.currentSong
|
||||||
parent = savedState.parent
|
// parent = savedState.parent
|
||||||
queue.applySavedState(savedState.queueState)
|
// queue.applySavedState(savedState.queueState)
|
||||||
repeatMode = savedState.repeatMode
|
// repeatMode = savedState.repeatMode
|
||||||
notifyNewPlayback()
|
// notifyNewPlayback()
|
||||||
|
//
|
||||||
// Check if we need to reload the player with a new music file, or if we can just leave
|
// // Check if we need to reload the player with a new music file, or if we can just
|
||||||
// it be. Specifically done so we don't pause on music updates that don't really change
|
// leave
|
||||||
// what's playing (ex. playlist editing)
|
// // it be. Specifically done so we don't pause on music updates that don't really
|
||||||
if (lastSong != queue.currentSong) {
|
// change
|
||||||
logD("Song changed, must reload player")
|
// // what's playing (ex. playlist editing)
|
||||||
// Continuing playback while also possibly doing drastic state updates is
|
// if (lastSong != queue.currentSong) {
|
||||||
// a bad idea, so pause.
|
// logD("Song changed, must reload player")
|
||||||
internalPlayer.loadSong(queue.currentSong, false)
|
// // Continuing playback while also possibly doing drastic state updates is
|
||||||
if (queue.currentSong != null) {
|
// // a bad idea, so pause.
|
||||||
logD("Seeking to saved position ${savedState.positionMs}ms")
|
// stateHolder.loadSong(queue.currentSong, false)
|
||||||
// Internal player may have reloaded the media item, re-seek to the previous
|
// if (queue.currentSong != null) {
|
||||||
// position
|
// logD("Seeking to saved position ${savedState.positionMs}ms")
|
||||||
seekTo(savedState.positionMs)
|
// // Internal player may have reloaded the media item, re-seek to the
|
||||||
}
|
// previous
|
||||||
}
|
// // position
|
||||||
|
// seekTo(savedState.positionMs)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
isInitialized = true
|
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)
|
* @author media3 team, Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class BetterShuffleOrder
|
class BetterShuffleOrder private constructor(private val shuffled: IntArray) : ShuffleOrder {
|
||||||
private constructor(private val shuffled: IntArray, private val random: Random) : ShuffleOrder {
|
|
||||||
private val indexInShuffled: IntArray = IntArray(shuffled.size)
|
private val indexInShuffled: IntArray = IntArray(shuffled.size)
|
||||||
|
|
||||||
/**
|
constructor(length: Int, startIndex: Int) : this(createShuffledList(length, startIndex))
|
||||||
* 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)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
for (i in shuffled.indices) {
|
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) {
|
for (i in 0 until insertionCount) {
|
||||||
newShuffled[pivot + i + 1] = insertionIndex + i + 1
|
newShuffled[pivot + i + 1] = insertionIndex + i + 1
|
||||||
}
|
}
|
||||||
return BetterShuffleOrder(newShuffled, Random(random.nextLong()))
|
return BetterShuffleOrder(newShuffled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cloneAndRemove(indexFrom: Int, indexToExclusive: Int): ShuffleOrder {
|
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]
|
else shuffled[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BetterShuffleOrder(newShuffled, Random(random.nextLong()))
|
return BetterShuffleOrder(newShuffled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cloneAndClear(): ShuffleOrder {
|
override fun cloneAndClear(): ShuffleOrder {
|
||||||
return BetterShuffleOrder(0, Random(random.nextLong()))
|
return BetterShuffleOrder(0, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun createShuffledList(length: Int, random: Random): IntArray {
|
private fun createShuffledList(length: Int, startIndex: Int): IntArray {
|
||||||
val shuffled = IntArray(length)
|
val shuffled = IntArray(length)
|
||||||
for (i in 0 until length) {
|
for (i in 0 until length) {
|
||||||
val swapIndex = random.nextInt(i + 1)
|
val swapIndex = (0..i).random()
|
||||||
shuffled[i] = shuffled[swapIndex]
|
shuffled[i] = shuffled[swapIndex]
|
||||||
shuffled[swapIndex] = i
|
shuffled[swapIndex] = i
|
||||||
}
|
}
|
||||||
|
if (startIndex != -1) {
|
||||||
|
val startIndexInShuffled = shuffled.indexOf(startIndex)
|
||||||
|
val temp = shuffled[0]
|
||||||
|
shuffled[0] = shuffled[startIndexInShuffled]
|
||||||
|
shuffled[startIndexInShuffled] = temp
|
||||||
|
}
|
||||||
return shuffled
|
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
|
// TODO: Figure this out
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
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.
|
// We have a song, so we can assume that the service will start a foreground state.
|
||||||
// At least, I hope. Again, *this is why we don't do this*. I cannot describe how
|
// At least, I hope. Again, *this is why we don't do this*. I cannot describe how
|
||||||
// stupid this is with the state of foreground services on modern android. One
|
// stupid this is with the state of foreground services on modern android. One
|
||||||
|
|
|
@ -38,9 +38,10 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.resolveNames
|
import org.oxycblt.auxio.music.resolveNames
|
||||||
import org.oxycblt.auxio.playback.ActionMode
|
import org.oxycblt.auxio.playback.ActionMode
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
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.InternalPlayer
|
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
import org.oxycblt.auxio.playback.state.Queue
|
||||||
|
import org.oxycblt.auxio.playback.state.QueueChange
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -117,65 +118,65 @@ constructor(
|
||||||
|
|
||||||
// --- PLAYBACKSTATEMANAGER OVERRIDES ---
|
// --- PLAYBACKSTATEMANAGER OVERRIDES ---
|
||||||
|
|
||||||
override fun onIndexMoved(queue: Queue) {
|
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||||
updateMediaMetadata(queue.currentSong, playbackManager.parent)
|
when (event) {
|
||||||
invalidateSessionState()
|
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 onQueueChanged(queue: Queue, change: Queue.Change) {
|
invalidateSecondaryAction()
|
||||||
updateQueue(queue)
|
}
|
||||||
when (change.type) {
|
|
||||||
// Nothing special to do with mapping changes.
|
|
||||||
Queue.Change.Type.MAPPING -> {}
|
|
||||||
// Index changed, ensure playback state's index changes.
|
|
||||||
Queue.Change.Type.INDEX -> invalidateSessionState()
|
|
||||||
// Song changed, ensure metadata changes.
|
|
||||||
Queue.Change.Type.SONG -> updateMediaMetadata(queue.currentSong, playbackManager.parent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueueReordered(queue: Queue) {
|
|
||||||
updateQueue(queue)
|
|
||||||
invalidateSessionState()
|
|
||||||
mediaSession.setShuffleMode(
|
|
||||||
if (queue.isShuffled) {
|
|
||||||
PlaybackStateCompat.SHUFFLE_MODE_ALL
|
|
||||||
} else {
|
|
||||||
PlaybackStateCompat.SHUFFLE_MODE_NONE
|
|
||||||
})
|
|
||||||
invalidateSecondaryAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
|
||||||
updateMediaMetadata(queue.currentSong, parent)
|
|
||||||
updateQueue(queue)
|
|
||||||
invalidateSessionState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStateChanged(state: InternalPlayer.State) {
|
|
||||||
invalidateSessionState()
|
|
||||||
notification.updatePlaying(playbackManager.playerState.isPlaying)
|
|
||||||
if (!bitmapProvider.isBusy) {
|
|
||||||
listener?.onPostNotification(notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRepeatChanged(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 ---
|
// --- SETTINGS OVERRIDES ---
|
||||||
|
|
||||||
override fun onImageSettingsChanged() {
|
override fun onImageSettingsChanged() {
|
||||||
// Need to reload the metadata cover.
|
// Need to reload the metadata cover.
|
||||||
updateMediaMetadata(playbackManager.queue.currentSong, playbackManager.parent)
|
updateMediaMetadata(playbackManager.currentSong, playbackManager.parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationActionChanged() {
|
override fun onNotificationActionChanged() {
|
||||||
|
@ -211,11 +212,11 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
playbackManager.setPlaying(true)
|
playbackManager.playing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
playbackManager.setPlaying(false)
|
playbackManager.playing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToNext() {
|
override fun onSkipToNext() {
|
||||||
|
@ -236,17 +237,17 @@ constructor(
|
||||||
|
|
||||||
override fun onRewind() {
|
override fun onRewind() {
|
||||||
playbackManager.rewind()
|
playbackManager.rewind()
|
||||||
playbackManager.setPlaying(true)
|
playbackManager.playing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetRepeatMode(repeatMode: Int) {
|
override fun onSetRepeatMode(repeatMode: Int) {
|
||||||
playbackManager.repeatMode =
|
playbackManager.repeatMode(
|
||||||
when (repeatMode) {
|
when (repeatMode) {
|
||||||
PlaybackStateCompat.REPEAT_MODE_ALL -> RepeatMode.ALL
|
PlaybackStateCompat.REPEAT_MODE_ALL -> RepeatMode.ALL
|
||||||
PlaybackStateCompat.REPEAT_MODE_GROUP -> RepeatMode.ALL
|
PlaybackStateCompat.REPEAT_MODE_GROUP -> RepeatMode.ALL
|
||||||
PlaybackStateCompat.REPEAT_MODE_ONE -> RepeatMode.TRACK
|
PlaybackStateCompat.REPEAT_MODE_ONE -> RepeatMode.TRACK
|
||||||
else -> RepeatMode.NONE
|
else -> RepeatMode.NONE
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetShuffleMode(shuffleMode: Int) {
|
override fun onSetShuffleMode(shuffleMode: Int) {
|
||||||
|
@ -356,7 +357,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
private fun updateQueue(queue: Queue) {
|
private fun updateQueue(queue: Queue) {
|
||||||
val queueItems =
|
val queueItems =
|
||||||
queue.resolve().mapIndexed { i, song ->
|
queue.queue.mapIndexed { i, song ->
|
||||||
val description =
|
val description =
|
||||||
MediaDescriptionCompat.Builder()
|
MediaDescriptionCompat.Builder()
|
||||||
// Media ID should not be the item index but rather the UID,
|
// Media ID should not be the item index but rather the UID,
|
||||||
|
@ -381,13 +382,15 @@ constructor(
|
||||||
private fun invalidateSessionState() {
|
private fun invalidateSessionState() {
|
||||||
logD("Updating media session playback state")
|
logD("Updating media session playback state")
|
||||||
|
|
||||||
|
val queue = playbackManager.resolveQueue()
|
||||||
|
|
||||||
val state =
|
val state =
|
||||||
// InternalPlayer.State handles position/state information.
|
// InternalPlayer.State handles position/state information.
|
||||||
playbackManager.playerState
|
playbackManager.progression
|
||||||
.intoPlaybackState(PlaybackStateCompat.Builder())
|
.intoPlaybackState(PlaybackStateCompat.Builder())
|
||||||
.setActions(ACTIONS)
|
.setActions(ACTIONS)
|
||||||
// Active queue ID corresponds to the indices we populated prior, use them here.
|
// Active queue ID corresponds to the indices we populated prior, use them here.
|
||||||
.setActiveQueueItemId(playbackManager.queue.index.toLong())
|
.setActiveQueueItemId(queue.index.toLong())
|
||||||
|
|
||||||
// Android 13+ relies on custom actions in the notification.
|
// Android 13+ relies on custom actions in the notification.
|
||||||
|
|
||||||
|
@ -399,7 +402,7 @@ constructor(
|
||||||
PlaybackStateCompat.CustomAction.Builder(
|
PlaybackStateCompat.CustomAction.Builder(
|
||||||
PlaybackService.ACTION_INVERT_SHUFFLE,
|
PlaybackService.ACTION_INVERT_SHUFFLE,
|
||||||
context.getString(R.string.desc_shuffle),
|
context.getString(R.string.desc_shuffle),
|
||||||
if (playbackManager.queue.isShuffled) {
|
if (playbackManager.isShuffled) {
|
||||||
R.drawable.ic_shuffle_on_24
|
R.drawable.ic_shuffle_on_24
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ic_shuffle_off_24
|
R.drawable.ic_shuffle_off_24
|
||||||
|
@ -435,7 +438,7 @@ constructor(
|
||||||
when (playbackSettings.notificationAction) {
|
when (playbackSettings.notificationAction) {
|
||||||
ActionMode.SHUFFLE -> {
|
ActionMode.SHUFFLE -> {
|
||||||
logD("Using shuffle notification action")
|
logD("Using shuffle notification action")
|
||||||
notification.updateShuffled(playbackManager.queue.isShuffled)
|
notification.updateShuffled(playbackManager.isShuffled)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
logD("Using repeat mode notification action")
|
logD("Using repeat mode notification action")
|
||||||
|
|
|
@ -48,13 +48,20 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.list.ListSettings
|
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.MusicRepository
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
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.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.RepeatMode
|
||||||
import org.oxycblt.auxio.service.ForegroundManager
|
import org.oxycblt.auxio.service.ForegroundManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -82,7 +89,7 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
||||||
class PlaybackService :
|
class PlaybackService :
|
||||||
Service(),
|
Service(),
|
||||||
Player.Listener,
|
Player.Listener,
|
||||||
InternalPlayer,
|
PlaybackStateHolder,
|
||||||
MediaSessionComponent.Listener,
|
MediaSessionComponent.Listener,
|
||||||
MusicRepository.UpdateListener {
|
MusicRepository.UpdateListener {
|
||||||
// Player components
|
// Player components
|
||||||
|
@ -148,7 +155,7 @@ class PlaybackService :
|
||||||
foregroundManager = ForegroundManager(this)
|
foregroundManager = ForegroundManager(this)
|
||||||
// Initialize any listener-dependent components last as we wouldn't want a listener race
|
// 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.
|
// condition to cause us to load music before we were fully initialize.
|
||||||
playbackManager.registerInternalPlayer(this)
|
playbackManager.registerStateHolder(this)
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
mediaSessionComponent.registerListener(this)
|
mediaSessionComponent.registerListener(this)
|
||||||
|
|
||||||
|
@ -189,8 +196,8 @@ class PlaybackService :
|
||||||
foregroundManager.release()
|
foregroundManager.release()
|
||||||
|
|
||||||
// Pause just in case this destruction was unexpected.
|
// Pause just in case this destruction was unexpected.
|
||||||
playbackManager.setPlaying(false)
|
playbackManager.playing(false)
|
||||||
playbackManager.unregisterInternalPlayer(this)
|
playbackManager.unregisterStateHolder(this)
|
||||||
musicRepository.removeUpdateListener(this)
|
musicRepository.removeUpdateListener(this)
|
||||||
|
|
||||||
unregisterReceiver(systemReceiver)
|
unregisterReceiver(systemReceiver)
|
||||||
|
@ -210,101 +217,221 @@ class PlaybackService :
|
||||||
logD("Service destroyed")
|
logD("Service destroyed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CONTROLLER OVERRIDES ---
|
// --- PLAYBACKSTATEHOLDER OVERRIDES ---
|
||||||
|
|
||||||
|
override val currentSong
|
||||||
|
get() = player.song
|
||||||
|
|
||||||
|
override val repeatMode
|
||||||
|
get() = player.repeat
|
||||||
|
|
||||||
|
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(it.durationMs))
|
||||||
|
}
|
||||||
|
?: Progression.nil()
|
||||||
|
|
||||||
override val audioSessionId: Int
|
override val audioSessionId: Int
|
||||||
get() = player.audioSessionId
|
get() = player.audioSessionId
|
||||||
|
|
||||||
override val shouldRewindWithPrev: Boolean
|
override var parent: MusicParent? = null
|
||||||
get() = playbackSettings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
|
||||||
|
|
||||||
override fun getState(durationMs: Long) =
|
override val isShuffled
|
||||||
InternalPlayer.State.from(
|
get() = player.shuffleModeEnabled
|
||||||
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) {
|
override fun resolveQueue(): Queue =
|
||||||
if (song == null) {
|
player.song?.let { Queue(player.currentIndex, player.resolveQueue()) } ?: Queue.nil()
|
||||||
// No song, stop playback and foreground state.
|
|
||||||
logD("Nothing playing, stopping playback")
|
override fun newPlayback(
|
||||||
// For some reason the player does not mark playWhenReady as false when stopped,
|
queue: List<Song>,
|
||||||
// which then completely breaks any re-initialization if playback starts again.
|
start: Song?,
|
||||||
// So we manually set it to false here.
|
parent: MusicParent?,
|
||||||
player.playWhenReady = false
|
shuffled: Boolean,
|
||||||
player.stop()
|
play: Boolean
|
||||||
stopAndSave()
|
) {
|
||||||
return
|
this.parent = parent
|
||||||
|
if (shuffled) {
|
||||||
|
player.shuffledQueue(queue, start)
|
||||||
|
} else {
|
||||||
|
player.orderedQueue(queue, start)
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Loading $song")
|
|
||||||
player.setMediaItem(MediaItem.fromUri(song.uri))
|
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.playWhenReady = play
|
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) {
|
override fun seekTo(positionMs: Long) {
|
||||||
logD("Seeking to ${positionMs}ms")
|
|
||||||
player.seekTo(positionMs)
|
player.seekTo(positionMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setPlaying(isPlaying: Boolean) {
|
override fun next() {
|
||||||
logD("Updating player state to $isPlaying")
|
player.seekToNext()
|
||||||
player.playWhenReady = isPlaying
|
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 ---
|
// --- PLAYER OVERRIDES ---
|
||||||
|
|
||||||
|
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
|
||||||
|
logD("Player has started playing")
|
||||||
|
if (!openAudioEffectSession) {
|
||||||
|
// Convention to start an audioeffect session on play/pause rather than
|
||||||
|
// start/stop
|
||||||
|
logD("Opening audio effect session")
|
||||||
|
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
||||||
|
openAudioEffectSession = true
|
||||||
|
}
|
||||||
|
} else if (openAudioEffectSession) {
|
||||||
|
// Make sure to close the audio session when we stop playback.
|
||||||
|
logD("Closing audio effect session")
|
||||||
|
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
||||||
|
openAudioEffectSession = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
super.onEvents(player, events)
|
super.onEvents(player, events)
|
||||||
if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
|
|
||||||
if (player.playWhenReady) {
|
|
||||||
// Mark that we have started playing so that the notification can now be posted.
|
|
||||||
hasPlayed = true
|
|
||||||
logD("Player has started playing")
|
|
||||||
if (!openAudioEffectSession) {
|
|
||||||
// Convention to start an audioeffect session on play/pause rather than
|
|
||||||
// start/stop
|
|
||||||
logD("Opening audio effect session")
|
|
||||||
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
|
||||||
openAudioEffectSession = true
|
|
||||||
}
|
|
||||||
} else if (openAudioEffectSession) {
|
|
||||||
// Make sure to close the audio session when we stop playback.
|
|
||||||
logD("Closing audio effect session")
|
|
||||||
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
|
||||||
openAudioEffectSession = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any change to the analogous isPlaying, isAdvancing, or positionMs values require
|
|
||||||
// us to synchronize with a new state.
|
|
||||||
if (events.containsAny(
|
if (events.containsAny(
|
||||||
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
||||||
Player.EVENT_IS_PLAYING_CHANGED,
|
Player.EVENT_IS_PLAYING_CHANGED,
|
||||||
Player.EVENT_POSITION_DISCONTINUITY)) {
|
Player.EVENT_POSITION_DISCONTINUITY)) {
|
||||||
logD("Player state changed, must synchronize state")
|
logD("Player state changed, must synchronize state")
|
||||||
playbackManager.synchronizeState(this)
|
playbackManager.dispatchEvent(this, PlaybackEvent.ProgressionChanged(progression))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,7 +474,7 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performAction(action: InternalPlayer.Action): Boolean {
|
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
||||||
val deviceLibrary =
|
val deviceLibrary =
|
||||||
musicRepository.deviceLibrary
|
musicRepository.deviceLibrary
|
||||||
// No library, cannot do anything.
|
// No library, cannot do anything.
|
||||||
|
@ -355,7 +482,7 @@ class PlaybackService :
|
||||||
|
|
||||||
when (action) {
|
when (action) {
|
||||||
// Restore state -> Start a new restoreState job
|
// Restore state -> Start a new restoreState job
|
||||||
is InternalPlayer.Action.RestoreState -> {
|
is DeferredPlayback.RestoreState -> {
|
||||||
logD("Restoring playback state")
|
logD("Restoring playback state")
|
||||||
restoreScope.launch {
|
restoreScope.launch {
|
||||||
persistenceRepository.readState()?.let {
|
persistenceRepository.readState()?.let {
|
||||||
|
@ -366,20 +493,20 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Shuffle all -> Start new playback from all songs
|
// Shuffle all -> Start new playback from all songs
|
||||||
is InternalPlayer.Action.ShuffleAll -> {
|
is DeferredPlayback.ShuffleAll -> {
|
||||||
logD("Shuffling all tracks")
|
logD("Shuffling all tracks")
|
||||||
playbackManager.play(
|
playbackManager.play(
|
||||||
null, null, listSettings.songSort.songs(deviceLibrary.songs), true)
|
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
|
// 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")
|
logD("Opening specified file")
|
||||||
deviceLibrary.findSongForUri(application, action.uri)?.let { song ->
|
deviceLibrary.findSongForUri(application, action.uri)?.let { song ->
|
||||||
playbackManager.play(
|
playbackManager.play(
|
||||||
song,
|
song,
|
||||||
null,
|
null,
|
||||||
listSettings.songSort.songs(deviceLibrary.songs),
|
listSettings.songSort.songs(deviceLibrary.songs),
|
||||||
playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
|
player.shuffleModeEnabled && playbackSettings.keepShuffle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -437,15 +564,15 @@ class PlaybackService :
|
||||||
// --- AUXIO EVENTS ---
|
// --- AUXIO EVENTS ---
|
||||||
ACTION_PLAY_PAUSE -> {
|
ACTION_PLAY_PAUSE -> {
|
||||||
logD("Received play event")
|
logD("Received play event")
|
||||||
playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
|
playbackManager.playing(!playbackManager.progression.isPlaying)
|
||||||
}
|
}
|
||||||
ACTION_INC_REPEAT_MODE -> {
|
ACTION_INC_REPEAT_MODE -> {
|
||||||
logD("Received repeat mode event")
|
logD("Received repeat mode event")
|
||||||
playbackManager.repeatMode = playbackManager.repeatMode.increment()
|
playbackManager.repeatMode(playbackManager.repeatMode.increment())
|
||||||
}
|
}
|
||||||
ACTION_INVERT_SHUFFLE -> {
|
ACTION_INVERT_SHUFFLE -> {
|
||||||
logD("Received shuffle event")
|
logD("Received shuffle event")
|
||||||
playbackManager.reorder(!playbackManager.queue.isShuffled)
|
playbackManager.reorder(!playbackManager.isShuffled)
|
||||||
}
|
}
|
||||||
ACTION_SKIP_PREV -> {
|
ACTION_SKIP_PREV -> {
|
||||||
logD("Received skip previous event")
|
logD("Received skip previous event")
|
||||||
|
@ -457,7 +584,7 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
ACTION_EXIT -> {
|
ACTION_EXIT -> {
|
||||||
logD("Received exit event")
|
logD("Received exit event")
|
||||||
playbackManager.setPlaying(false)
|
playbackManager.playing(false)
|
||||||
stopAndSave()
|
stopAndSave()
|
||||||
}
|
}
|
||||||
WidgetProvider.ACTION_WIDGET_UPDATE -> {
|
WidgetProvider.ACTION_WIDGET_UPDATE -> {
|
||||||
|
@ -472,17 +599,17 @@ class PlaybackService :
|
||||||
// which would result in unexpected playback. Work around it by dropping the first
|
// which would result in unexpected playback. Work around it by dropping the first
|
||||||
// call to this function, which should come from that Intent.
|
// call to this function, which should come from that Intent.
|
||||||
if (playbackSettings.headsetAutoplay &&
|
if (playbackSettings.headsetAutoplay &&
|
||||||
playbackManager.queue.currentSong != null &&
|
playbackManager.currentSong != null &&
|
||||||
initialHeadsetPlugEventHandled) {
|
initialHeadsetPlugEventHandled) {
|
||||||
logD("Device connected, resuming")
|
logD("Device connected, resuming")
|
||||||
playbackManager.setPlaying(true)
|
playbackManager.playing(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pauseFromHeadsetPlug() {
|
private fun pauseFromHeadsetPlug() {
|
||||||
if (playbackManager.queue.currentSong != null) {
|
if (playbackManager.currentSong != null) {
|
||||||
logD("Device disconnected, pausing")
|
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.ImageSettings
|
||||||
import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
|
import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
|
||||||
import org.oxycblt.auxio.image.extractor.SquareCropTransformation
|
import org.oxycblt.auxio.image.extractor.SquareCropTransformation
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.queue.Queue
|
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.PlaybackStateManager
|
||||||
|
import org.oxycblt.auxio.playback.state.QueueChange
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.ui.UISettings
|
import org.oxycblt.auxio.ui.UISettings
|
||||||
import org.oxycblt.auxio.util.getDimenPixels
|
import org.oxycblt.auxio.util.getDimenPixels
|
||||||
|
@ -64,7 +64,7 @@ constructor(
|
||||||
|
|
||||||
/** Update [WidgetProvider] with the current playback state. */
|
/** Update [WidgetProvider] with the current playback state. */
|
||||||
fun update() {
|
fun update() {
|
||||||
val song = playbackManager.queue.currentSong
|
val song = playbackManager.currentSong
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
logD("No song, resetting widget")
|
logD("No song, resetting widget")
|
||||||
widgetProvider.update(context, uiSettings, null)
|
widgetProvider.update(context, uiSettings, null)
|
||||||
|
@ -72,9 +72,9 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Store these values here so they remain consistent once the bitmap is loaded.
|
// Note: Store these values here so they remain consistent once the bitmap is loaded.
|
||||||
val isPlaying = playbackManager.playerState.isPlaying
|
val isPlaying = playbackManager.progression.isPlaying
|
||||||
val repeatMode = playbackManager.repeatMode
|
val repeatMode = playbackManager.repeatMode
|
||||||
val isShuffled = playbackManager.queue.isShuffled
|
val isShuffled = playbackManager.isShuffled
|
||||||
|
|
||||||
logD("Updating widget with new playback state")
|
logD("Updating widget with new playback state")
|
||||||
bitmapProvider.load(
|
bitmapProvider.load(
|
||||||
|
@ -135,16 +135,16 @@ constructor(
|
||||||
|
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
// Respond to all major song or player changes that will affect the widget
|
override fun onPlaybackEvent(event: PlaybackEvent) {
|
||||||
override fun onIndexMoved(queue: Queue) = update()
|
if (event is PlaybackEvent.NewPlayback ||
|
||||||
|
event is PlaybackEvent.ProgressionChanged ||
|
||||||
override fun onQueueReordered(queue: Queue) = update()
|
(event is PlaybackEvent.QueueChanged && event.change.type == QueueChange.Type.SONG) ||
|
||||||
|
event is PlaybackEvent.QueueReordered ||
|
||||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) = update()
|
event is PlaybackEvent.IndexMoved ||
|
||||||
|
event is PlaybackEvent.RepeatModeChanged) {
|
||||||
override fun onStateChanged(state: InternalPlayer.State) = update()
|
update()
|
||||||
|
}
|
||||||
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
|
}
|
||||||
|
|
||||||
// Respond to settings changes that will affect the widget
|
// Respond to settings changes that will affect the widget
|
||||||
override fun onRoundModeChanged() = update()
|
override fun onRoundModeChanged() = update()
|
||||||
|
@ -156,7 +156,7 @@ constructor(
|
||||||
*
|
*
|
||||||
* @param song [Queue.currentSong]
|
* @param song [Queue.currentSong]
|
||||||
* @param cover A pre-loaded album cover [Bitmap] for [song].
|
* @param cover A pre-loaded album cover [Bitmap] for [song].
|
||||||
* @param isPlaying [PlaybackStateManager.playerState]
|
* @param isPlaying [PlaybackStateManager.progression]
|
||||||
* @param repeatMode [PlaybackStateManager.repeatMode]
|
* @param repeatMode [PlaybackStateManager.repeatMode]
|
||||||
* @param isShuffled [Queue.isShuffled]
|
* @param isShuffled [Queue.isShuffled]
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue