playback: migrate to reactive model
Migrate the playback system to a reactive model where internalPlayer is now the complete source of truth for the playback state. This removes the observer pattern for positions and instead introduces a new State datatype that allows consumers to reactively calculate where the position probably is. This is actually really great for efficiency and state coherency, and is really what I was trying to aim for with previous (failed) reworks to the playback system. There's probably some bugs, but way less than the ground-up rewrites I tried before. This also lays the groundwork for gapless playback, as the internal player framework is now completely capable of having more functionality borged into it.
This commit is contained in:
parent
a9bbdff25d
commit
e5d7cdc340
9 changed files with 212 additions and 172 deletions
|
|
@ -45,8 +45,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* - The RecyclerView data for each fragment
|
* - The RecyclerView data for each fragment
|
||||||
* - The sorts for each type of data
|
* - The sorts for each type of data
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*
|
|
||||||
* TODO: Unify how detail items are indicated [When playlists are implemented]
|
|
||||||
*/
|
*/
|
||||||
class DetailViewModel(application: Application) :
|
class DetailViewModel(application: Application) :
|
||||||
AndroidViewModel(application), MusicStore.Callback {
|
AndroidViewModel(application), MusicStore.Callback {
|
||||||
|
|
|
||||||
|
|
@ -189,11 +189,7 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
val id = tag.key.sanitize().uppercase()
|
val id = tag.key.sanitize().uppercase()
|
||||||
val value = tag.value.sanitize()
|
val value = tag.value.sanitize()
|
||||||
if (value.isNotEmpty()) {
|
if (value.isNotEmpty()) {
|
||||||
if (vorbisTags.containsKey(id)) {
|
vorbisTags.getOrPut(id) { mutableListOf() }.add(value)
|
||||||
vorbisTags[id]!!.add(value)
|
|
||||||
} else {
|
|
||||||
vorbisTags[id] = mutableListOf(value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ package org.oxycblt.auxio.playback
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -78,6 +80,8 @@ class PlaybackViewModel(application: Application) :
|
||||||
val currentAudioSessionId: Int?
|
val currentAudioSessionId: Int?
|
||||||
get() = playbackManager.currentAudioSessionId
|
get() = playbackManager.currentAudioSessionId
|
||||||
|
|
||||||
|
private var lastPositionJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +213,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
|
|
||||||
/** Flip the playing status, e.g from playing to paused */
|
/** Flip the playing status, e.g from playing to paused */
|
||||||
fun invertPlaying() {
|
fun invertPlaying() {
|
||||||
playbackManager.isPlaying = !playbackManager.isPlaying
|
playbackManager.changePlaying(!playbackManager.playerState.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Flip the shuffle status, e.g from on to off. Will keep song by default. */
|
/** Flip the shuffle status, e.g from on to off. Will keep song by default. */
|
||||||
|
|
@ -268,12 +272,18 @@ class PlaybackViewModel(application: Application) :
|
||||||
_parent.value = playbackManager.parent
|
_parent.value = playbackManager.parent
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionChanged(positionMs: Long) {
|
override fun onStateChanged(state: InternalPlayer.State) {
|
||||||
_positionDs.value = positionMs.msToDs()
|
_isPlaying.value = state.isPlaying
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlayingChanged(isPlaying: Boolean) {
|
// Start watching the position again
|
||||||
_isPlaying.value = isPlaying
|
lastPositionJob?.cancel()
|
||||||
|
lastPositionJob =
|
||||||
|
viewModelScope.launch {
|
||||||
|
while (true) {
|
||||||
|
_positionDs.value = state.calculateElapsedPosition().msToDs()
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShuffledChanged(isShuffled: Boolean) {
|
override fun onShuffledChanged(isShuffled: Boolean) {
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,6 @@ import org.oxycblt.auxio.util.logD
|
||||||
* Instead, we wrap it in a safe class that hopefully implements enough sanity checks to not crash
|
* Instead, we wrap it in a safe class that hopefully implements enough sanity checks to not crash
|
||||||
* the app or result in blatantly janky behavior. Mostly.
|
* the app or result in blatantly janky behavior. Mostly.
|
||||||
*
|
*
|
||||||
* TODO: Add smooth seeking
|
|
||||||
*
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class StyledSeekBar
|
class StyledSeekBar
|
||||||
|
|
@ -69,15 +67,19 @@ constructor(
|
||||||
var positionDs: Long
|
var positionDs: Long
|
||||||
get() = binding.seekBarSlider.value.toLong()
|
get() = binding.seekBarSlider.value.toLong()
|
||||||
set(value) {
|
set(value) {
|
||||||
|
// Sanity check 1: Ensure that no negative values are sneaking their way into
|
||||||
|
// this component.
|
||||||
|
val from = max(value, 0)
|
||||||
|
|
||||||
// Sanity check: Ensure that this value is within the duration and will not crash
|
// Sanity check: Ensure that this value is within the duration and will not crash
|
||||||
// the app, and that the user is not currently seeking (which would cause the SeekBar
|
// the app, and that the user is not currently seeking (which would cause the SeekBar
|
||||||
// to jump around).
|
// to jump around).
|
||||||
if (value <= durationDs && !isActivated) {
|
if (from <= durationDs && !isActivated) {
|
||||||
binding.seekBarSlider.value = value.toFloat()
|
binding.seekBarSlider.value = from.toFloat()
|
||||||
|
|
||||||
// We would want to keep this in the callback, but the callback only fires when
|
// We would want to keep this in the callback, but the callback only fires when
|
||||||
// a value changes completely, and sometimes that does not happen with this view.
|
// a value changes completely, and sometimes that does not happen with this view.
|
||||||
binding.seekBarPosition.text = value.formatDurationDs(true)
|
binding.seekBarPosition.text = from.formatDurationDs(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
/** Represents a class capable of managing the internal player. */
|
/** Represents a class capable of managing the internal player. */
|
||||||
|
|
@ -28,14 +30,16 @@ interface InternalPlayer {
|
||||||
/** Whether the player should rewind instead of going to the previous song. */
|
/** Whether the player should rewind instead of going to the previous song. */
|
||||||
val shouldRewindWithPrev: Boolean
|
val shouldRewindWithPrev: Boolean
|
||||||
|
|
||||||
|
val currentState: State
|
||||||
|
|
||||||
/** Called when a new song should be loaded into the player. */
|
/** Called when a new song should be loaded into the player. */
|
||||||
fun loadSong(song: Song?)
|
fun loadSong(song: Song?, play: Boolean)
|
||||||
|
|
||||||
/** Seek to [positionMs] in the player. */
|
/** Seek to [positionMs] in the player. */
|
||||||
fun seekTo(positionMs: Long)
|
fun seekTo(positionMs: Long)
|
||||||
|
|
||||||
/** Called when the playing state is changed. */
|
/** Called when the playing state needs to be changed. */
|
||||||
fun onPlayingChanged(isPlaying: Boolean)
|
fun changePlaying(isPlaying: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when [PlaybackStateManager] desires some [Action] to be completed. Returns true if the
|
* Called when [PlaybackStateManager] desires some [Action] to be completed. Returns true if the
|
||||||
|
|
@ -43,6 +47,77 @@ interface InternalPlayer {
|
||||||
*/
|
*/
|
||||||
fun onAction(action: Action): Boolean
|
fun onAction(action: Action): Boolean
|
||||||
|
|
||||||
|
class State
|
||||||
|
private constructor(
|
||||||
|
/**
|
||||||
|
* Whether the user has actually chosen to play this audio. The player might not actually be
|
||||||
|
* playing at this time.
|
||||||
|
*/
|
||||||
|
val isPlaying: Boolean,
|
||||||
|
/** Whether the player is actually advancing through the audio. */
|
||||||
|
private val isAdvancing: Boolean,
|
||||||
|
/** The initial position at update time. */
|
||||||
|
private val initPositionMs: Long,
|
||||||
|
/** The time this instance was created. */
|
||||||
|
private val creationTime: Long
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Calculate the estimated position that the player is now at. If the player's position is
|
||||||
|
* not advancing, this will be the initial position. Otherwise, this will be the position
|
||||||
|
* plus the elapsed time since this state was uploaded.
|
||||||
|
*/
|
||||||
|
fun calculateElapsedPosition() =
|
||||||
|
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 state into the analogous [PlaybackStateCompat.Builder]. */
|
||||||
|
fun intoPlaybackState(builder: PlaybackStateCompat.Builder): PlaybackStateCompat.Builder =
|
||||||
|
builder.setState(
|
||||||
|
if (isPlaying) {
|
||||||
|
PlaybackStateCompat.STATE_PLAYING
|
||||||
|
} else {
|
||||||
|
PlaybackStateCompat.STATE_PAUSED
|
||||||
|
},
|
||||||
|
initPositionMs,
|
||||||
|
if (isAdvancing) {
|
||||||
|
1f
|
||||||
|
} else {
|
||||||
|
// Not advancing, so don't move the position.
|
||||||
|
0f
|
||||||
|
},
|
||||||
|
creationTime)
|
||||||
|
|
||||||
|
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 of this state. */
|
||||||
|
fun new(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) =
|
||||||
|
State(
|
||||||
|
// Minor sanity check: Make sure that advancing can't occur if the
|
||||||
|
// main playing value is paused.
|
||||||
|
isPlaying,
|
||||||
|
isPlaying && isAdvancing,
|
||||||
|
positionMs,
|
||||||
|
SystemClock.elapsedRealtime())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Action {
|
sealed class Action {
|
||||||
object RestoreState : Action()
|
object RestoreState : Action()
|
||||||
object ShuffleAll : Action()
|
object ShuffleAll : Action()
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ import org.oxycblt.auxio.util.logW
|
||||||
*/
|
*/
|
||||||
class PlaybackStateManager private constructor() {
|
class PlaybackStateManager private constructor() {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
private var internalPlayer: InternalPlayer? = null
|
||||||
|
|
||||||
/** The currently playing song. Null if there isn't one */
|
/** The currently playing song. Null if there isn't one */
|
||||||
val song
|
val song
|
||||||
|
|
@ -67,14 +69,10 @@ class PlaybackStateManager private constructor() {
|
||||||
var index = -1
|
var index = -1
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/** Whether playback is playing or not */
|
/** The current state of the internal player. */
|
||||||
var isPlaying = false
|
var playerState = InternalPlayer.State.new(isPlaying = false, isAdvancing = false, 0)
|
||||||
set(value) {
|
private set
|
||||||
field = value
|
|
||||||
notifyPlayingChanged()
|
|
||||||
}
|
|
||||||
/** The current playback progress */
|
|
||||||
private var positionMs = 0L
|
|
||||||
/** The current [RepeatMode] */
|
/** The current [RepeatMode] */
|
||||||
var repeatMode = RepeatMode.NONE
|
var repeatMode = RepeatMode.NONE
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|
@ -94,22 +92,16 @@ class PlaybackStateManager private constructor() {
|
||||||
get() = internalPlayer?.audioSessionId
|
get() = internalPlayer?.audioSessionId
|
||||||
|
|
||||||
/** An action that is awaiting the internal player instance to consume it. */
|
/** An action that is awaiting the internal player instance to consume it. */
|
||||||
var pendingAction: InternalPlayer.Action? = null
|
private var pendingAction: InternalPlayer.Action? = null
|
||||||
|
|
||||||
// --- CALLBACKS ---
|
|
||||||
|
|
||||||
private val callbacks = mutableListOf<Callback>()
|
|
||||||
private var internalPlayer: InternalPlayer? = null
|
|
||||||
|
|
||||||
/** Add a callback to this instance. Make sure to remove it when done. */
|
/** Add a callback to this instance. Make sure to remove it when done. */
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun addCallback(callback: Callback) {
|
fun addCallback(callback: Callback) {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
callback.onNewPlayback(index, queue, parent)
|
callback.onNewPlayback(index, queue, parent)
|
||||||
callback.onPositionChanged(positionMs)
|
|
||||||
callback.onPlayingChanged(isPlaying)
|
|
||||||
callback.onRepeatChanged(repeatMode)
|
callback.onRepeatChanged(repeatMode)
|
||||||
callback.onShuffledChanged(isShuffled)
|
callback.onShuffledChanged(isShuffled)
|
||||||
|
callback.onStateChanged(playerState)
|
||||||
}
|
}
|
||||||
|
|
||||||
callbacks.add(callback)
|
callbacks.add(callback)
|
||||||
|
|
@ -130,10 +122,10 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
internalPlayer.loadSong(song)
|
internalPlayer.loadSong(song, playerState.isPlaying)
|
||||||
internalPlayer.seekTo(positionMs)
|
internalPlayer.seekTo(playerState.calculateElapsedPosition())
|
||||||
internalPlayer.onPlayingChanged(isPlaying)
|
|
||||||
requestAction(internalPlayer)
|
requestAction(internalPlayer)
|
||||||
|
synchronizeState(internalPlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.internalPlayer = internalPlayer
|
this.internalPlayer = internalPlayer
|
||||||
|
|
@ -155,6 +147,7 @@ class PlaybackStateManager private constructor() {
|
||||||
/** Play a [song]. */
|
/** Play a [song]. */
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun play(song: Song, playbackMode: PlaybackMode, settings: Settings) {
|
fun play(song: Song, playbackMode: PlaybackMode, settings: Settings) {
|
||||||
|
val internalPlayer = internalPlayer ?: return
|
||||||
val library = musicStore.library ?: return
|
val library = musicStore.library ?: return
|
||||||
|
|
||||||
parent =
|
parent =
|
||||||
|
|
@ -169,33 +162,44 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
applyNewQueue(library, settings, settings.keepShuffle && isShuffled, song)
|
applyNewQueue(library, settings, settings.keepShuffle && isShuffled, song)
|
||||||
|
|
||||||
notifyNewPlayback()
|
notifyNewPlayback()
|
||||||
notifyShuffledChanged()
|
notifyShuffledChanged()
|
||||||
isPlaying = true
|
|
||||||
|
internalPlayer.loadSong(song, true)
|
||||||
|
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Play a [parent], such as an artist or album. */
|
/** Play a [parent], such as an artist or album. */
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun play(parent: MusicParent, shuffled: Boolean, settings: Settings) {
|
fun play(parent: MusicParent, shuffled: Boolean, settings: Settings) {
|
||||||
|
val internalPlayer = internalPlayer ?: return
|
||||||
val library = musicStore.library ?: return
|
val library = musicStore.library ?: return
|
||||||
|
|
||||||
this.parent = parent
|
this.parent = parent
|
||||||
applyNewQueue(library, settings, shuffled, null)
|
applyNewQueue(library, settings, shuffled, null)
|
||||||
|
|
||||||
notifyNewPlayback()
|
notifyNewPlayback()
|
||||||
notifyShuffledChanged()
|
notifyShuffledChanged()
|
||||||
isPlaying = true
|
|
||||||
|
internalPlayer.loadSong(song, true)
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shuffle all songs. */
|
/** Shuffle all songs. */
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun shuffleAll(settings: Settings) {
|
fun shuffleAll(settings: Settings) {
|
||||||
|
val internalPlayer = internalPlayer ?: return
|
||||||
val library = musicStore.library ?: return
|
val library = musicStore.library ?: return
|
||||||
|
|
||||||
parent = null
|
parent = null
|
||||||
applyNewQueue(library, settings, true, null)
|
applyNewQueue(library, settings, true, null)
|
||||||
|
|
||||||
notifyNewPlayback()
|
notifyNewPlayback()
|
||||||
notifyShuffledChanged()
|
notifyShuffledChanged()
|
||||||
isPlaying = true
|
|
||||||
|
internalPlayer.loadSong(song, true)
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,36 +208,41 @@ class PlaybackStateManager private constructor() {
|
||||||
/** Go to the next song, along with doing all the checks that entails. */
|
/** Go to the next song, along with doing all the checks that entails. */
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun next() {
|
fun next() {
|
||||||
|
val internalPlayer = internalPlayer ?: return
|
||||||
|
|
||||||
// Increment the index, if it cannot be incremented any further, then
|
// Increment the index, if it cannot be incremented any further, then
|
||||||
// repeat and pause/resume playback depending on the setting
|
// repeat and pause/resume playback depending on the setting
|
||||||
if (index < _queue.lastIndex) {
|
if (index < _queue.lastIndex) {
|
||||||
gotoImpl(index + 1, true)
|
gotoImpl(internalPlayer, index + 1, true)
|
||||||
} else {
|
} else {
|
||||||
gotoImpl(0, repeatMode == RepeatMode.ALL)
|
gotoImpl(internalPlayer, 0, repeatMode == RepeatMode.ALL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Go to the previous song, doing any checks that are needed. */
|
/** Go to the previous song, doing any checks that are needed. */
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun prev() {
|
fun prev() {
|
||||||
|
val internalPlayer = internalPlayer ?: return
|
||||||
|
|
||||||
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
|
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
|
||||||
if (internalPlayer?.shouldRewindWithPrev == true) {
|
if (internalPlayer.shouldRewindWithPrev) {
|
||||||
rewind()
|
rewind()
|
||||||
isPlaying = true
|
changePlaying(true)
|
||||||
} else {
|
} else {
|
||||||
gotoImpl(max(index - 1, 0), true)
|
gotoImpl(internalPlayer, max(index - 1, 0), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun goto(index: Int) {
|
fun goto(index: Int) {
|
||||||
gotoImpl(index, true)
|
val internalPlayer = internalPlayer ?: return
|
||||||
|
gotoImpl(internalPlayer, index, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun gotoImpl(idx: Int, play: Boolean) {
|
private fun gotoImpl(internalPlayer: InternalPlayer, idx: Int, play: Boolean) {
|
||||||
index = idx
|
index = idx
|
||||||
notifyIndexMoved()
|
notifyIndexMoved()
|
||||||
isPlaying = play
|
internalPlayer.loadSong(song, play)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a [song] to the top of the queue. */
|
/** Add a [song] to the top of the queue. */
|
||||||
|
|
@ -330,19 +339,17 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
// --- INTERNAL PLAYER FUNCTIONS ---
|
// --- INTERNAL PLAYER FUNCTIONS ---
|
||||||
|
|
||||||
/** Update the current [positionMs]. Only meant for use by [InternalPlayer] */
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun synchronizePosition(internalPlayer: InternalPlayer, positionMs: Long) {
|
fun synchronizeState(internalPlayer: InternalPlayer) {
|
||||||
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
|
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
|
||||||
logW("Given internal player did not match current internal player")
|
logW("Given internal player did not match current internal player")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't accept any bugged positions that are over the duration of the song.
|
val newState = internalPlayer.currentState
|
||||||
val maxDuration = song?.durationMs ?: -1
|
if (newState != playerState) {
|
||||||
if (positionMs <= maxDuration) {
|
playerState = newState
|
||||||
this.positionMs = positionMs
|
notifyStateChanged()
|
||||||
notifyPositionChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,7 +357,7 @@ class PlaybackStateManager private constructor() {
|
||||||
fun startAction(action: InternalPlayer.Action) {
|
fun startAction(action: InternalPlayer.Action) {
|
||||||
val internalPlayer = internalPlayer
|
val internalPlayer = internalPlayer
|
||||||
if (internalPlayer == null || !internalPlayer.onAction(action)) {
|
if (internalPlayer == null || !internalPlayer.onAction(action)) {
|
||||||
logD("Internal player not present or did not consume action, ignoring")
|
logD("Internal player not present or did not consume action, waiting")
|
||||||
pendingAction = action
|
pendingAction = action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -369,15 +376,18 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Change the current playing state. */
|
||||||
|
fun changePlaying(isPlaying: Boolean) {
|
||||||
|
internalPlayer?.changePlaying(isPlaying)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **Seek** to a [positionMs].
|
* **Seek** to a [positionMs].
|
||||||
* @param positionMs The position to seek to in millis.
|
* @param positionMs The position to seek to in millis.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun seekTo(positionMs: Long) {
|
fun seekTo(positionMs: Long) {
|
||||||
this.positionMs = positionMs
|
|
||||||
internalPlayer?.seekTo(positionMs)
|
internalPlayer?.seekTo(positionMs)
|
||||||
notifyPositionChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rewind to the beginning of a song. */
|
/** Rewind to the beginning of a song. */
|
||||||
|
|
@ -392,14 +402,13 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val library = musicStore.library ?: return false
|
val library = musicStore.library ?: return false
|
||||||
|
val internalPlayer = internalPlayer ?: return false
|
||||||
val state = withContext(Dispatchers.IO) { database.read(library) }
|
val state = withContext(Dispatchers.IO) { database.read(library) }
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (state != null && (!isInitialized || force)) {
|
if (state != null && (!isInitialized || force)) {
|
||||||
// Continuing playback while also possibly doing drastic state updates is
|
// Continuing playback while also possibly doing drastic state updates is
|
||||||
// a bad idea, so pause.
|
// a bad idea, so pause.
|
||||||
isPlaying = false
|
|
||||||
|
|
||||||
index = state.index
|
index = state.index
|
||||||
parent = state.parent
|
parent = state.parent
|
||||||
_queue = state.queue.toMutableList()
|
_queue = state.queue.toMutableList()
|
||||||
|
|
@ -407,10 +416,12 @@ class PlaybackStateManager private constructor() {
|
||||||
isShuffled = state.isShuffled
|
isShuffled = state.isShuffled
|
||||||
|
|
||||||
notifyNewPlayback()
|
notifyNewPlayback()
|
||||||
seekTo(state.positionMs)
|
|
||||||
notifyRepeatModeChanged()
|
notifyRepeatModeChanged()
|
||||||
notifyShuffledChanged()
|
notifyShuffledChanged()
|
||||||
|
|
||||||
|
internalPlayer.loadSong(song, false)
|
||||||
|
internalPlayer.seekTo(state.positionMs)
|
||||||
|
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
@ -440,13 +451,15 @@ class PlaybackStateManager private constructor() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val internalPlayer = internalPlayer ?: return
|
||||||
|
|
||||||
logD("Sanitizing state")
|
logD("Sanitizing state")
|
||||||
|
|
||||||
// While we could just save and reload the state, we instead sanitize the state
|
// While we could just save and reload the state, we instead sanitize the state
|
||||||
// at runtime for better efficiency (and to sidestep a co-routine on behalf of the caller).
|
// at runtime for better performance (and to sidestep a co-routine on behalf of the caller).
|
||||||
|
|
||||||
val oldSongId = song?.id
|
val oldSongId = song?.id
|
||||||
val oldPosition = positionMs
|
val oldPosition = playerState.calculateElapsedPosition()
|
||||||
|
|
||||||
parent =
|
parent =
|
||||||
parent?.let {
|
parent?.let {
|
||||||
|
|
@ -463,10 +476,11 @@ class PlaybackStateManager private constructor() {
|
||||||
index--
|
index--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyNewPlayback()
|
||||||
|
|
||||||
// Continuing playback while also possibly doing drastic state updates is
|
// Continuing playback while also possibly doing drastic state updates is
|
||||||
// a bad idea, so pause.
|
// a bad idea, so pause.
|
||||||
isPlaying = false
|
internalPlayer.loadSong(song, false)
|
||||||
notifyNewPlayback()
|
|
||||||
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
// Internal player may have reloaded the media item, re-seek to the previous position
|
// Internal player may have reloaded the media item, re-seek to the previous position
|
||||||
|
|
@ -479,14 +493,13 @@ class PlaybackStateManager private constructor() {
|
||||||
index = index,
|
index = index,
|
||||||
parent = parent,
|
parent = parent,
|
||||||
queue = _queue,
|
queue = _queue,
|
||||||
positionMs = positionMs,
|
positionMs = playerState.calculateElapsedPosition(),
|
||||||
isShuffled = isShuffled,
|
isShuffled = isShuffled,
|
||||||
repeatMode = repeatMode)
|
repeatMode = repeatMode)
|
||||||
|
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
private fun notifyIndexMoved() {
|
private fun notifyIndexMoved() {
|
||||||
internalPlayer?.loadSong(song)
|
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onIndexMoved(index)
|
callback.onIndexMoved(index)
|
||||||
}
|
}
|
||||||
|
|
@ -505,22 +518,14 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyNewPlayback() {
|
private fun notifyNewPlayback() {
|
||||||
internalPlayer?.loadSong(song)
|
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onNewPlayback(index, queue, parent)
|
callback.onNewPlayback(index, queue, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyPlayingChanged() {
|
private fun notifyStateChanged() {
|
||||||
internalPlayer?.onPlayingChanged(isPlaying)
|
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onPlayingChanged(isPlaying)
|
callback.onStateChanged(playerState)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyPositionChanged() {
|
|
||||||
for (callback in callbacks) {
|
|
||||||
callback.onPositionChanged(positionMs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -553,11 +558,8 @@ class PlaybackStateManager private constructor() {
|
||||||
/** Called when playback is changed completely, with a new index, queue, and parent. */
|
/** Called when playback is changed completely, with a new index, queue, and parent. */
|
||||||
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
|
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
|
||||||
|
|
||||||
/** Called when the playing state is changed. */
|
/** Called when the state of the internal player changes. */
|
||||||
fun onPlayingChanged(isPlaying: Boolean) {}
|
fun onStateChanged(state: InternalPlayer.State) {}
|
||||||
|
|
||||||
/** Called when the position is re-synchronized by the internal player. */
|
|
||||||
fun onPositionChanged(positionMs: Long) {}
|
|
||||||
|
|
||||||
/** Called when the repeat mode is changed. */
|
/** Called when the repeat mode is changed. */
|
||||||
fun onRepeatChanged(repeatMode: RepeatMode) {}
|
fun onRepeatChanged(repeatMode: RepeatMode) {}
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,17 @@ import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.SystemClock
|
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import androidx.media.session.MediaButtonReceiver
|
import androidx.media.session.MediaButtonReceiver
|
||||||
import com.google.android.exoplayer2.Player
|
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.image.BitmapProvider
|
import org.oxycblt.auxio.image.BitmapProvider
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
|
|
@ -59,26 +58,16 @@ import org.oxycblt.auxio.util.logD
|
||||||
* Replace it. Please.
|
* Replace it. Please.
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*
|
|
||||||
* TODO: Remove the player callback once smooth seeking is implemented
|
|
||||||
*/
|
*/
|
||||||
class MediaSessionComponent(
|
class MediaSessionComponent(private val context: Context, private val callback: Callback) :
|
||||||
private val context: Context,
|
MediaSessionCompat.Callback(), PlaybackStateManager.Callback, Settings.Callback {
|
||||||
private val player: Player,
|
|
||||||
private val callback: Callback
|
|
||||||
) :
|
|
||||||
Player.Listener,
|
|
||||||
MediaSessionCompat.Callback(),
|
|
||||||
PlaybackStateManager.Callback,
|
|
||||||
Settings.Callback {
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onPostNotification(notification: NotificationComponent?, reason: PostingReason)
|
fun onPostNotification(notification: NotificationComponent?, reason: PostingReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class PostingReason {
|
enum class PostingReason {
|
||||||
METADATA,
|
METADATA,
|
||||||
ACTIONS,
|
ACTIONS
|
||||||
POSITION
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mediaSession =
|
private val mediaSession =
|
||||||
|
|
@ -94,7 +83,6 @@ class MediaSessionComponent(
|
||||||
private val provider = BitmapProvider(context)
|
private val provider = BitmapProvider(context)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
player.addListener(this)
|
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
mediaSession.setCallback(this)
|
mediaSession.setCallback(this)
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +94,6 @@ class MediaSessionComponent(
|
||||||
fun release() {
|
fun release() {
|
||||||
provider.release()
|
provider.release()
|
||||||
settings.release()
|
settings.release()
|
||||||
player.removeListener(this)
|
|
||||||
playbackManager.removeCallback(this)
|
playbackManager.removeCallback(this)
|
||||||
|
|
||||||
mediaSession.apply {
|
mediaSession.apply {
|
||||||
|
|
@ -225,13 +212,10 @@ class MediaSessionComponent(
|
||||||
mediaSession.setQueue(queueItems)
|
mediaSession.setQueue(queueItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayingChanged(isPlaying: Boolean) {
|
override fun onStateChanged(state: InternalPlayer.State) {
|
||||||
invalidateSessionState()
|
invalidateSessionState()
|
||||||
notification.updatePlaying(playbackManager.isPlaying)
|
notification.updatePlaying(playbackManager.playerState.isPlaying)
|
||||||
|
|
||||||
if (!provider.isBusy) {
|
if (!provider.isBusy) {
|
||||||
// Still probably want to start the notification though regardless of the version,
|
|
||||||
// as playback is starting.
|
|
||||||
callback.onPostNotification(notification, PostingReason.ACTIONS)
|
callback.onPostNotification(notification, PostingReason.ACTIONS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,25 +253,6 @@ class MediaSessionComponent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- EXOPLAYER CALLBACKS ---
|
|
||||||
|
|
||||||
override fun onPositionDiscontinuity(
|
|
||||||
oldPosition: Player.PositionInfo,
|
|
||||||
newPosition: Player.PositionInfo,
|
|
||||||
reason: Int
|
|
||||||
) {
|
|
||||||
invalidateSessionState()
|
|
||||||
|
|
||||||
if (!playbackManager.isPlaying) {
|
|
||||||
// Hack around issue where the position won't update after a seek when paused.
|
|
||||||
// Apparently this can be fixed by re-posting the notification, but not always
|
|
||||||
// when we invalidate the state (that will cause us to be rate-limited), and also not
|
|
||||||
// always when we seek (that will also cause us to be rate-limited). Someone looked at
|
|
||||||
// this system and said it was well-designed.
|
|
||||||
callback.onPostNotification(notification, PostingReason.POSITION)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- MEDIASESSION CALLBACKS ---
|
// --- MEDIASESSION CALLBACKS ---
|
||||||
|
|
||||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
|
|
@ -316,11 +281,11 @@ class MediaSessionComponent(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
playbackManager.isPlaying = true
|
playbackManager.changePlaying(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
playbackManager.isPlaying = false
|
playbackManager.changePlaying(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToNext() {
|
override fun onSkipToNext() {
|
||||||
|
|
@ -341,7 +306,7 @@ class MediaSessionComponent(
|
||||||
|
|
||||||
override fun onRewind() {
|
override fun onRewind() {
|
||||||
playbackManager.rewind()
|
playbackManager.rewind()
|
||||||
playbackManager.isPlaying = true
|
playbackManager.changePlaying(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetRepeatMode(repeatMode: Int) {
|
override fun onSetRepeatMode(repeatMode: Int) {
|
||||||
|
|
@ -391,17 +356,9 @@ class MediaSessionComponent(
|
||||||
val state =
|
val state =
|
||||||
PlaybackStateCompat.Builder()
|
PlaybackStateCompat.Builder()
|
||||||
.setActions(ACTIONS)
|
.setActions(ACTIONS)
|
||||||
.setBufferedPosition(player.bufferedPosition)
|
|
||||||
.setActiveQueueItemId(playbackManager.index.toLong())
|
.setActiveQueueItemId(playbackManager.index.toLong())
|
||||||
|
|
||||||
val playerState =
|
playbackManager.playerState.intoPlaybackState(state)
|
||||||
if (playbackManager.isPlaying) {
|
|
||||||
PlaybackStateCompat.STATE_PLAYING
|
|
||||||
} else {
|
|
||||||
PlaybackStateCompat.STATE_PAUSED
|
|
||||||
}
|
|
||||||
|
|
||||||
state.setState(playerState, player.currentPosition, 1.0f, SystemClock.elapsedRealtime())
|
|
||||||
|
|
||||||
// Android 13+ leverages custom actions in the notification.
|
// Android 13+ leverages custom actions in the notification.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,10 @@ import com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
|
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
|
||||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||||
|
import kotlin.math.max
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
@ -102,7 +102,6 @@ class PlaybackService :
|
||||||
|
|
||||||
// Coroutines
|
// Coroutines
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val positionScope = CoroutineScope(serviceJob + Dispatchers.Main)
|
|
||||||
private val restoreScope = CoroutineScope(serviceJob + Dispatchers.Main)
|
private val restoreScope = CoroutineScope(serviceJob + Dispatchers.Main)
|
||||||
private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main)
|
private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main)
|
||||||
|
|
||||||
|
|
@ -149,15 +148,9 @@ class PlaybackService :
|
||||||
|
|
||||||
playbackManager.registerInternalPlayer(this)
|
playbackManager.registerInternalPlayer(this)
|
||||||
musicStore.addCallback(this)
|
musicStore.addCallback(this)
|
||||||
positionScope.launch {
|
|
||||||
while (true) {
|
|
||||||
playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition)
|
|
||||||
delay(POS_POLL_INTERVAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
widgetComponent = WidgetComponent(this)
|
widgetComponent = WidgetComponent(this)
|
||||||
mediaSessionComponent = MediaSessionComponent(this, player, this)
|
mediaSessionComponent = MediaSessionComponent(this, this)
|
||||||
|
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
||||||
|
|
@ -197,10 +190,10 @@ class PlaybackService :
|
||||||
foregroundManager.release()
|
foregroundManager.release()
|
||||||
|
|
||||||
// Pause just in case this destruction was unexpected.
|
// Pause just in case this destruction was unexpected.
|
||||||
playbackManager.isPlaying = false
|
playbackManager.changePlaying(false)
|
||||||
|
|
||||||
playbackManager.unregisterInternalPlayer(this)
|
playbackManager.unregisterInternalPlayer(this)
|
||||||
musicStore.addCallback(this)
|
musicStore.removeCallback(this)
|
||||||
settings.release()
|
settings.release()
|
||||||
unregisterReceiver(systemReceiver)
|
unregisterReceiver(systemReceiver)
|
||||||
serviceJob.cancel()
|
serviceJob.cancel()
|
||||||
|
|
@ -220,15 +213,21 @@ class PlaybackService :
|
||||||
|
|
||||||
// --- PLAYER OVERRIDES ---
|
// --- PLAYER OVERRIDES ---
|
||||||
|
|
||||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
super.onEvents(player, events)
|
||||||
|
|
||||||
if (playWhenReady) {
|
var needToSynchronize =
|
||||||
hasPlayed = true
|
events.containsAny(Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_POSITION_DISCONTINUITY)
|
||||||
|
|
||||||
|
if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
|
||||||
|
needToSynchronize = true
|
||||||
|
if (player.playWhenReady) {
|
||||||
|
hasPlayed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackManager.isPlaying != playWhenReady) {
|
if (needToSynchronize) {
|
||||||
playbackManager.isPlaying = playWhenReady
|
playbackManager.synchronizeState(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,7 +236,7 @@ class PlaybackService :
|
||||||
if (playbackManager.repeatMode == RepeatMode.TRACK) {
|
if (playbackManager.repeatMode == RepeatMode.TRACK) {
|
||||||
playbackManager.rewind()
|
playbackManager.rewind()
|
||||||
if (settings.pauseOnRepeat) {
|
if (settings.pauseOnRepeat) {
|
||||||
playbackManager.isPlaying = false
|
playbackManager.changePlaying(false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
playbackManager.next()
|
playbackManager.next()
|
||||||
|
|
@ -251,14 +250,6 @@ class PlaybackService :
|
||||||
playbackManager.next()
|
playbackManager.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionDiscontinuity(
|
|
||||||
oldPosition: Player.PositionInfo,
|
|
||||||
newPosition: Player.PositionInfo,
|
|
||||||
reason: Int
|
|
||||||
) {
|
|
||||||
playbackManager.synchronizePosition(this, player.currentPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
super.onTracksChanged(tracks)
|
super.onTracksChanged(tracks)
|
||||||
|
|
||||||
|
|
@ -284,7 +275,12 @@ class PlaybackService :
|
||||||
override val shouldRewindWithPrev: Boolean
|
override val shouldRewindWithPrev: Boolean
|
||||||
get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
||||||
|
|
||||||
override fun loadSong(song: Song?) {
|
override val currentState: InternalPlayer.State
|
||||||
|
get() =
|
||||||
|
InternalPlayer.State.new(
|
||||||
|
player.playWhenReady, player.isPlaying, max(player.currentPosition, 0))
|
||||||
|
|
||||||
|
override fun loadSong(song: Song?, play: Boolean) {
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
// Stop the foreground state if there's nothing to play.
|
// Stop the foreground state if there's nothing to play.
|
||||||
logD("Nothing playing, stopping playback")
|
logD("Nothing playing, stopping playback")
|
||||||
|
|
@ -310,6 +306,8 @@ class PlaybackService :
|
||||||
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
||||||
openAudioEffectSession = true
|
openAudioEffectSession = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
player.playWhenReady = play
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun broadcastAudioEffectAction(event: String) {
|
private fun broadcastAudioEffectAction(event: String) {
|
||||||
|
|
@ -337,7 +335,7 @@ class PlaybackService :
|
||||||
player.seekTo(positionMs)
|
player.seekTo(positionMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayingChanged(isPlaying: Boolean) {
|
override fun changePlaying(isPlaying: Boolean) {
|
||||||
player.playWhenReady = isPlaying
|
player.playWhenReady = isPlaying
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -437,7 +435,8 @@ class PlaybackService :
|
||||||
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
|
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
|
||||||
|
|
||||||
// --- AUXIO EVENTS ---
|
// --- AUXIO EVENTS ---
|
||||||
ACTION_PLAY_PAUSE -> playbackManager.isPlaying = !playbackManager.isPlaying
|
ACTION_PLAY_PAUSE ->
|
||||||
|
playbackManager.changePlaying(!playbackManager.playerState.isPlaying)
|
||||||
ACTION_INC_REPEAT_MODE ->
|
ACTION_INC_REPEAT_MODE ->
|
||||||
playbackManager.repeatMode = playbackManager.repeatMode.increment()
|
playbackManager.repeatMode = playbackManager.repeatMode.increment()
|
||||||
ACTION_INVERT_SHUFFLE ->
|
ACTION_INVERT_SHUFFLE ->
|
||||||
|
|
@ -445,7 +444,7 @@ class PlaybackService :
|
||||||
ACTION_SKIP_PREV -> playbackManager.prev()
|
ACTION_SKIP_PREV -> playbackManager.prev()
|
||||||
ACTION_SKIP_NEXT -> playbackManager.next()
|
ACTION_SKIP_NEXT -> playbackManager.next()
|
||||||
ACTION_EXIT -> {
|
ACTION_EXIT -> {
|
||||||
playbackManager.isPlaying = false
|
playbackManager.changePlaying(false)
|
||||||
stopAndSave()
|
stopAndSave()
|
||||||
}
|
}
|
||||||
WidgetProvider.ACTION_WIDGET_UPDATE -> widgetComponent.update()
|
WidgetProvider.ACTION_WIDGET_UPDATE -> widgetComponent.update()
|
||||||
|
|
@ -466,7 +465,7 @@ class PlaybackService :
|
||||||
settings.headsetAutoplay &&
|
settings.headsetAutoplay &&
|
||||||
initialHeadsetPlugEventHandled) {
|
initialHeadsetPlugEventHandled) {
|
||||||
logD("Device connected, resuming")
|
logD("Device connected, resuming")
|
||||||
playbackManager.isPlaying = true
|
playbackManager.changePlaying(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -474,7 +473,7 @@ class PlaybackService :
|
||||||
private fun pauseFromPlug() {
|
private fun pauseFromPlug() {
|
||||||
if (playbackManager.song != null) {
|
if (playbackManager.song != null) {
|
||||||
logD("Device disconnected, pausing")
|
logD("Device disconnected, pausing")
|
||||||
playbackManager.isPlaying = false
|
playbackManager.changePlaying(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import org.oxycblt.auxio.image.BitmapProvider
|
||||||
import org.oxycblt.auxio.image.SquareFrameTransform
|
import org.oxycblt.auxio.image.SquareFrameTransform
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
|
|
@ -74,7 +75,7 @@ class WidgetComponent(private val context: Context) :
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Store these values here so they remain consistent once the bitmap is loaded.
|
// Note: Store these values here so they remain consistent once the bitmap is loaded.
|
||||||
val isPlaying = playbackManager.isPlaying
|
val isPlaying = playbackManager.playerState.isPlaying
|
||||||
val repeatMode = playbackManager.repeatMode
|
val repeatMode = playbackManager.repeatMode
|
||||||
val isShuffled = playbackManager.isShuffled
|
val isShuffled = playbackManager.isShuffled
|
||||||
|
|
||||||
|
|
@ -139,7 +140,7 @@ class WidgetComponent(private val context: Context) :
|
||||||
|
|
||||||
override fun onIndexMoved(index: Int) = update()
|
override fun onIndexMoved(index: Int) = update()
|
||||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) = update()
|
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) = update()
|
||||||
override fun onPlayingChanged(isPlaying: Boolean) = update()
|
override fun onStateChanged(state: InternalPlayer.State) = update()
|
||||||
override fun onShuffledChanged(isShuffled: Boolean) = update()
|
override fun onShuffledChanged(isShuffled: Boolean) = update()
|
||||||
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
|
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
|
||||||
override fun onSettingChanged(key: String) {
|
override fun onSettingChanged(key: String) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue