playback: rework fields

Rework the playback fields.

The new fields are more coherent, better-named, and less prone to state
failure.
This commit is contained in:
OxygenCobalt 2022-04-29 14:29:56 -06:00
parent 2bdbe212df
commit c80af01d5c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
10 changed files with 310 additions and 475 deletions

View file

@ -80,7 +80,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
binding.playbackSkipPrev?.setOnClickListener { playbackModel.skipPrev() }
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlayingStatus() }
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
binding.playbackSkipNext?.setOnClickListener { playbackModel.skipNext() }
@ -97,7 +97,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
playbackModel.isPlaying.observe(viewLifecycleOwner, ::updateIsPlaying)
playbackModel.positionSeconds.observe(viewLifecycleOwner, ::updatePosition)
playbackModel.positionSecs.observe(viewLifecycleOwner, ::updatePosition)
}
private fun updateSong(song: Song?) {

View file

@ -29,7 +29,6 @@ import kotlin.math.max
import org.oxycblt.auxio.R
import org.oxycblt.auxio.coil.bindAlbumCover
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration
@ -56,7 +55,6 @@ class PlaybackPanelFragment :
Slider.OnSliderTouchListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentPlaybackPanelBinding.inflate(inflater)
@ -114,26 +112,26 @@ class PlaybackPanelFragment :
.stateList
}
binding.playbackLoop.setOnClickListener { playbackModel.incrementLoopStatus() }
binding.playbackLoop.setOnClickListener { playbackModel.incrementLoop() }
binding.playbackSkipPrev.setOnClickListener { playbackModel.skipPrev() }
binding.playbackPlayPause.apply {
// Abuse the play/pause FAB (see style definition for more info)
post { binding.playbackPlayPause.stateListAnimator = null }
setOnClickListener { playbackModel.invertPlayingStatus() }
setOnClickListener { playbackModel.invertPlaying() }
}
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffleStatus() }
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
// --- VIEWMODEL SETUP --
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
playbackModel.positionSeconds.observe(viewLifecycleOwner, ::updatePosition)
playbackModel.positionSecs.observe(viewLifecycleOwner, ::updatePosition)
playbackModel.loopMode.observe(viewLifecycleOwner, ::updateLoop)
playbackModel.isPlaying.observe(viewLifecycleOwner, ::updatePlayPause)
playbackModel.isShuffling.observe(viewLifecycleOwner, ::updateShuffle)
playbackModel.isPlaying.observe(viewLifecycleOwner, ::updatePlaying)
playbackModel.isShuffled.observe(viewLifecycleOwner, ::updateShuffled)
playbackModel.nextUp.observe(viewLifecycleOwner) { nextUp ->
// The queue icon uses a selector that will automatically tint the icon as active or
@ -206,11 +204,11 @@ class PlaybackPanelFragment :
}
}
private fun updatePlayPause(isPlaying: Boolean) {
private fun updatePlaying(isPlaying: Boolean) {
requireBinding().playbackPlayPause.isActivated = isPlaying
}
private fun updateShuffle(isShuffling: Boolean) {
requireBinding().playbackShuffle.isActivated = isShuffling
private fun updateShuffled(isShuffled: Boolean) {
requireBinding().playbackShuffle.isActivated = isShuffled
}
}

View file

@ -61,16 +61,14 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// States
private val mIsPlaying = MutableLiveData(false)
private val mIsShuffling = MutableLiveData(false)
private val mPositionSecs = MutableLiveData(0L)
private val mLoopMode = MutableLiveData(LoopMode.NONE)
private val mPositionSeconds = MutableLiveData(0L)
private val mIsShuffled = MutableLiveData(false)
// Queue
private val mNextUp = MutableLiveData(listOf<Song>())
private val mMode = MutableLiveData(PlaybackMode.ALL_SONGS)
// Other
// TODO: Move URI management to PlaybackService (more capable of taking commands)
private var mIntentUri: Uri? = null
/** The current song. */
@ -82,21 +80,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
val isPlaying: LiveData<Boolean>
get() = mIsPlaying
val isShuffling: LiveData<Boolean>
get() = mIsShuffling
/** The current playback position, in seconds */
val positionSecs: LiveData<Long>
get() = mPositionSecs
/** The current repeat mode, see [LoopMode] for more information */
val loopMode: LiveData<LoopMode>
get() = mLoopMode
/** The current playback position, in seconds */
val positionSeconds: LiveData<Long>
get() = mPositionSeconds
val isShuffled: LiveData<Boolean>
get() = mIsShuffled
/** The queue, without the previous items. */
val nextUp: LiveData<List<Song>>
get() = mNextUp
/** The current [PlaybackMode] that also determines the queue */
val playbackMode: LiveData<PlaybackMode>
get() = mMode
init {
playbackManager.addCallback(this)
@ -117,7 +112,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* mode of the user if not specified.
*/
fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) {
playbackManager.playSong(song, mode)
playbackManager.play(song, mode)
}
/**
@ -131,7 +126,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
return
}
playbackManager.playParent(album, shuffled)
playbackManager.play(album, shuffled)
}
/**
@ -145,7 +140,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
return
}
playbackManager.playParent(artist, shuffled)
playbackManager.play(artist, shuffled)
}
/**
@ -159,7 +154,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
return
}
playbackManager.playParent(genre, shuffled)
playbackManager.play(genre, shuffled)
}
/**
@ -192,7 +187,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
/** Update the position and push it to [PlaybackStateManager] */
fun setPosition(progress: Long) {
playbackManager.seekTo((progress * 1000))
playbackManager.seekTo(progress * 1000)
}
// --- QUEUE FUNCTIONS ---
@ -229,7 +224,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
val to = adapterTo + delta
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) {
apply()
playbackManager.moveQueueItems(from, to)
playbackManager.moveQueueItem(from, to)
return true
}
@ -259,18 +254,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// --- STATUS FUNCTIONS ---
/** Flip the playing status, e.g from playing to paused */
fun invertPlayingStatus() {
playbackManager.setPlaying(!playbackManager.isPlaying)
fun invertPlaying() {
playbackManager.isPlaying = !playbackManager.isPlaying
}
/** Flip the shuffle status, e.g from on to off. Will keep song by default. */
fun invertShuffleStatus() {
playbackManager.setShuffling(!playbackManager.isShuffling, true)
fun invertShuffled() {
playbackManager.reshuffle(!playbackManager.isShuffled)
}
/** Increment the loop status, e.g from off to loop once */
fun incrementLoopStatus() {
playbackManager.setLoopMode(playbackManager.loopMode.increment())
fun incrementLoop() {
playbackManager.loopMode = playbackManager.loopMode.increment()
}
// --- SAVE/RESTORE FUNCTIONS ---
@ -314,12 +309,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
private fun restorePlaybackState() {
logD("Attempting to restore playback state")
onSongChanged(playbackManager.song)
onPositionChanged(playbackManager.position)
onParentChanged(playbackManager.parent)
onQueueChanged(playbackManager.queue, playbackManager.index)
onPositionChanged(playbackManager.positionMs)
onPlayingChanged(playbackManager.isPlaying)
onShuffleChanged(playbackManager.isShuffling)
onShuffledChanged(playbackManager.isShuffled)
onLoopModeChanged(playbackManager.loopMode)
}
@ -329,28 +321,32 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.removeCallback(this)
}
override fun onSongChanged(song: Song?) {
mSong.value = song
override fun onIndexMoved(index: Int) {
mSong.value = playbackManager.song
mNextUp.value = playbackManager.queue.slice(index.inc() until playbackManager.queue.size)
}
override fun onParentChanged(parent: MusicParent?) {
mParent.value = parent
}
override fun onPositionChanged(position: Long) {
mPositionSeconds.value = position / 1000
}
override fun onQueueChanged(queue: List<Song>, index: Int) {
override fun onQueueChanged(index: Int, queue: List<Song>) {
mSong.value = playbackManager.song
mNextUp.value = queue.slice(index.inc() until queue.size)
}
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
mParent.value = playbackManager.parent
mSong.value = playbackManager.song
mNextUp.value = queue.slice(index.inc() until queue.size)
}
override fun onPositionChanged(positionMs: Long) {
mPositionSecs.value = positionMs / 1000
}
override fun onPlayingChanged(isPlaying: Boolean) {
mIsPlaying.value = isPlaying
}
override fun onShuffleChanged(isShuffling: Boolean) {
mIsShuffling.value = isShuffling
override fun onShuffledChanged(isShuffled: Boolean) {
mIsShuffled.value = isShuffled
}
override fun onLoopModeChanged(loopMode: LoopMode) {

View file

@ -82,7 +82,7 @@ class PlaybackStateDatabase(context: Context) :
.append("${StateColumns.COLUMN_PARENT_HASH} LONG,")
.append("${StateColumns.COLUMN_QUEUE_INDEX} INTEGER NOT NULL,")
.append("${StateColumns.COLUMN_PLAYBACK_MODE} INTEGER NOT NULL,")
.append("${StateColumns.COLUMN_IS_SHUFFLING} BOOLEAN NOT NULL,")
.append("${StateColumns.COLUMN_IS_SHUFFLED} BOOLEAN NOT NULL,")
.append("${StateColumns.COLUMN_LOOP_MODE} INTEGER NOT NULL)")
return command
@ -118,7 +118,7 @@ class PlaybackStateDatabase(context: Context) :
val parentIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_HASH)
val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_QUEUE_INDEX)
val modeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PLAYBACK_MODE)
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLING)
val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLED)
val loopModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_LOOP_MODE)
cursor.moveToFirst()
@ -141,11 +141,11 @@ class PlaybackStateDatabase(context: Context) :
state =
SavedState(
song = song,
position = cursor.getLong(posIndex),
positionMs = cursor.getLong(posIndex),
parent = parent,
queueIndex = cursor.getInt(indexIndex),
playbackMode = mode,
isShuffling = cursor.getInt(shuffleIndex) == 1,
isShuffled = cursor.getInt(shuffleIndex) == 1,
loopMode = LoopMode.fromIntCode(cursor.getInt(loopModeIndex)) ?: LoopMode.NONE,
)
@ -168,11 +168,11 @@ class PlaybackStateDatabase(context: Context) :
ContentValues(10).apply {
put(StateColumns.COLUMN_ID, 0)
put(StateColumns.COLUMN_SONG_HASH, state.song?.id)
put(StateColumns.COLUMN_POSITION, state.position)
put(StateColumns.COLUMN_POSITION, state.positionMs)
put(StateColumns.COLUMN_PARENT_HASH, state.parent?.id)
put(StateColumns.COLUMN_QUEUE_INDEX, state.queueIndex)
put(StateColumns.COLUMN_PLAYBACK_MODE, state.playbackMode.intCode)
put(StateColumns.COLUMN_IS_SHUFFLING, state.isShuffling)
put(StateColumns.COLUMN_IS_SHUFFLED, state.isShuffled)
put(StateColumns.COLUMN_LOOP_MODE, state.loopMode.intCode)
}
@ -257,11 +257,11 @@ class PlaybackStateDatabase(context: Context) :
data class SavedState(
val song: Song?,
val position: Long,
val positionMs: Long,
val parent: MusicParent?,
val queueIndex: Int,
val playbackMode: PlaybackMode,
val isShuffling: Boolean,
val isShuffled: Boolean,
val loopMode: LoopMode,
)
@ -272,7 +272,7 @@ class PlaybackStateDatabase(context: Context) :
const val COLUMN_PARENT_HASH = "parent"
const val COLUMN_QUEUE_INDEX = "queue_index"
const val COLUMN_PLAYBACK_MODE = "playback_mode"
const val COLUMN_IS_SHUFFLING = "is_shuffling"
const val COLUMN_IS_SHUFFLED = "is_shuffling"
const val COLUMN_LOOP_MODE = "loop_mode"
}

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.playback.state
import android.content.Context
import kotlin.math.max
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.music.Album
@ -28,7 +29,6 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
/**
* Master class (and possible god object) for the playback state.
@ -47,54 +47,43 @@ class PlaybackStateManager private constructor() {
private val settingsManager = SettingsManager.getInstance()
// Playback
private var mSong: Song? = null
private var mParent: MusicParent? = null
// Queue
private var mQueue = mutableListOf<Song>()
private var mIndex = 0
// State
private var mIsPlaying = false
private var mPosition: Long = 0
private var mIsShuffling = false
private var mLoopMode = LoopMode.NONE
private var mIsRestored = false
private var mHasPlayed = false
private var mutableQueue = mutableListOf<Song>()
/** The currently playing song. Null if there isn't one */
val song: Song?
get() = mSong
/** The parent the queue is based on, null if all_songs */
val parent: MusicParent?
get() = mParent
val song
get() = queue.getOrNull(index)
/** The parent the queue is based on, null if all songs */
var parent: MusicParent? = null
private set
/** The current queue determined by [parent] */
val queue: List<Song>
get() = mQueue
val queue
get() = mutableQueue
/** The current position in the queue */
val index: Int
get() = mIndex
var index = -1
private set
/** Whether playback is paused or not */
val isPlaying: Boolean
get() = mIsPlaying
/** Whether playback is playing or not */
var isPlaying = false
set(value) {
field = value
notifyPlayingChanged()
}
/** The current playback progress */
val position: Long
get() = mPosition
var positionMs = 0L
private set
/** The current [LoopMode] */
val loopMode: LoopMode
get() = mLoopMode
var loopMode = LoopMode.NONE
set(value) {
field = value
notifyLoopModeChanged()
}
/** Whether the queue is shuffled */
val isShuffling: Boolean
get() = mIsShuffling
var isShuffled = false
private set
/** Whether this instance has already been restored */
val isRestored: Boolean
get() = mIsRestored
/** Whether playback has begun in this instance during **PlaybackService's Lifecycle.** */
val hasPlayed: Boolean
get() = mHasPlayed
var isRestored = false
private set
// --- CALLBACKS ---
@ -117,84 +106,46 @@ class PlaybackStateManager private constructor() {
/**
* Play a [song].
* @param mode The [PlaybackMode] to construct the queue off of.
* @param playbackMode The [PlaybackMode] to construct the queue off of.
*/
fun playSong(song: Song, mode: PlaybackMode) {
logD("Updating song to ${song.rawName} and mode to $mode")
fun play(song: Song, playbackMode: PlaybackMode) {
val library = musicStore.library ?: return
when (mode) {
PlaybackMode.ALL_SONGS -> {
val musicStore = musicStore.library ?: return
mParent = null
mQueue = musicStore.songs.toMutableList()
}
PlaybackMode.IN_GENRE -> {
mParent = song.genre
mQueue = song.genre.songs.toMutableList()
}
PlaybackMode.IN_ARTIST -> {
mParent = song.album.artist
mQueue = song.album.artist.songs.toMutableList()
}
PlaybackMode.IN_ALBUM -> {
mParent = song.album
mQueue = song.album.songs.toMutableList()
}
parent =
when (playbackMode) {
PlaybackMode.ALL_SONGS -> null
PlaybackMode.IN_ALBUM -> song.album
PlaybackMode.IN_ARTIST -> song.album.artist
PlaybackMode.IN_GENRE -> song.genre
}
notifyParentChanged()
updatePlayback(song)
// Keep shuffle on, if enabled
setShuffling(settingsManager.keepShuffle && isShuffling, keepSong = true)
applyNewQueue(library, settingsManager.keepShuffle && isShuffled, song, true)
notifyNewPlayback()
notifyShuffledChanged()
isPlaying = true
}
/**
* Play a [parent], such as an artist or album.
* @param shuffled Whether the queue is shuffled or not
*/
fun playParent(parent: MusicParent, shuffled: Boolean) {
logD("Playing ${parent.rawName}")
mParent = parent
notifyParentChanged()
mIndex = 0
mQueue =
when (parent) {
is Album -> {
parent.songs.toMutableList()
}
is Artist -> {
parent.songs.toMutableList()
}
is Genre -> {
parent.songs.toMutableList()
}
}
setShuffling(shuffled, keepSong = false)
updatePlayback(mQueue[0])
fun play(parent: MusicParent?, shuffled: Boolean) {
val library = musicStore.library ?: return
this.parent = parent
applyNewQueue(library, shuffled, null, true)
notifyNewPlayback()
notifyShuffledChanged()
isPlaying = true
}
/** Shuffle all songs. */
fun shuffleAll() {
val library = musicStore.library ?: return
mQueue = library.songs.toMutableList()
mParent = null
setShuffling(true, keepSong = false)
updatePlayback(mQueue[0])
}
/** Update the playback to a new [song], doing all the required logic. */
private fun updatePlayback(song: Song, shouldPlay: Boolean = true) {
mSong = song
mPosition = 0
notifySongChanged()
notifyPositionChanged()
setPlaying(shouldPlay)
parent = null
applyNewQueue(library, true, null, true)
notifyNewPlayback()
notifyShuffledChanged()
isPlaying = true
}
// --- QUEUE FUNCTIONS ---
@ -203,226 +154,162 @@ class PlaybackStateManager private constructor() {
fun next() {
// Increment the index, if it cannot be incremented any further, then
// loop and pause/resume playback depending on the setting
if (mIndex < mQueue.lastIndex) {
mIndex = mIndex.inc()
updatePlayback(mQueue[mIndex])
if (index < mutableQueue.lastIndex) {
goto(++index, true)
} else {
mIndex = 0
updatePlayback(mQueue[mIndex], shouldPlay = loopMode == LoopMode.ALL)
goto(0, loopMode == LoopMode.ALL)
}
notifyQueueChanged()
}
/** Go to the previous song, doing any checks that are needed. */
fun prev() {
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
if (settingsManager.rewindWithPrev && mPosition >= REWIND_THRESHOLD) {
if (settingsManager.rewindWithPrev && positionMs >= REWIND_THRESHOLD) {
rewind()
isPlaying = true
} else {
// Only decrement the index if there's a song to move back to
if (mIndex > 0) {
mIndex = mIndex.dec()
}
updatePlayback(mQueue[mIndex])
notifyQueueChanged()
goto(max(--index, 0), true)
}
}
// --- QUEUE EDITING FUNCTIONS ---
/** Remove a queue item at [index]. Will ignore invalid indexes. */
fun removeQueueItem(index: Int): Boolean {
if (index > mQueue.size || index < 0) {
logE("Index is out of bounds, did not remove queue item")
return false
}
logD("Removing item ${mQueue[index].rawName}")
mQueue.removeAt(index)
notifyQueueChanged()
return true
}
/** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */
fun moveQueueItems(from: Int, to: Int): Boolean {
if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) {
logE("Indices were out of bounds, did not move queue item")
return false
}
logD("Moving item $from to position $to")
mQueue.add(to, mQueue.removeAt(from))
notifyQueueChanged()
return true
private fun goto(idx: Int, play: Boolean) {
index = idx
notifyIndexMoved()
isPlaying = play
}
/** Add a [song] to the top of the queue. */
fun playNext(song: Song) {
if (mQueue.isEmpty()) {
return
}
mQueue.add(mIndex + 1, song)
mutableQueue.add(++index, song)
notifyQueueChanged()
}
/** Add a list of [songs] to the top of the queue. */
fun playNext(songs: List<Song>) {
if (mQueue.isEmpty()) {
return
}
mQueue.addAll(mIndex + 1, songs)
mutableQueue.addAll(++index, songs)
notifyQueueChanged()
}
/** Add a [song] to the end of the queue. */
fun addToQueue(song: Song) {
mQueue.add(song)
mutableQueue.add(song)
notifyQueueChanged()
}
/** Add a list of [songs] to the end of the queue. */
fun addToQueue(songs: List<Song>) {
mQueue.addAll(songs)
mutableQueue.addAll(songs)
notifyQueueChanged()
}
// --- SHUFFLE FUNCTIONS ---
/**
* Set whether this instance is [shuffled]. Updates the queue accordingly.
* @param keepSong Whether the current song should be kept as the queue is shuffled/un-shuffled
*/
fun setShuffling(shuffled: Boolean, keepSong: Boolean) {
mIsShuffling = shuffled
notifyShufflingChanged()
if (mIsShuffling) {
genShuffle(keepSong)
} else {
resetShuffle(keepSong)
}
}
/**
* Generate a new shuffled queue.
* @param keepSong Whether the current song should be kept as the queue is shuffled
*/
private fun genShuffle(keepSong: Boolean) {
val lastSong = mSong
logD("Shuffling queue")
mQueue.shuffle()
mIndex = 0
// If specified, make the current song the first member of the queue.
if (keepSong) {
moveQueueItems(mQueue.indexOf(lastSong), 0)
} else {
// Otherwise, just start from the zeroth position in the queue.
mSong = mQueue[0]
}
/** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */
fun moveQueueItem(from: Int, to: Int) {
logD("Moving item $from to position $to")
mutableQueue.add(to, mutableQueue.removeAt(from))
notifyQueueChanged()
}
/**
* Reset the queue to its normal, ordered state.
* @param keepSong Whether the current song should be kept as the queue is un-shuffled
*/
private fun resetShuffle(keepSong: Boolean) {
/** Remove a queue item at [index]. Will ignore invalid indexes. */
fun removeQueueItem(index: Int) {
logD("Removing item ${mutableQueue[index].rawName}")
mutableQueue.removeAt(index)
notifyQueueChanged()
}
/** Set whether this instance is [shuffled]. Updates the queue accordingly. */
fun reshuffle(shuffled: Boolean) {
val library = musicStore.library ?: return
val lastSong = mSong
val parent = parent
mQueue =
when (parent) {
null -> settingsManager.libSongSort.songs(library.songs).toMutableList()
is Album -> settingsManager.detailAlbumSort.album(parent).toMutableList()
is Artist -> settingsManager.detailArtistSort.artist(parent).toMutableList()
is Genre -> settingsManager.detailGenreSort.genre(parent).toMutableList()
}
if (keepSong) {
mIndex = mQueue.indexOf(lastSong)
}
val song = song ?: return
applyNewQueue(library, shuffled, song, false)
notifyQueueChanged()
notifyShuffledChanged()
}
private fun applyNewQueue(
library: MusicStore.Library,
shuffled: Boolean,
keep: Song?,
regenShuffledQueue: Boolean
) {
if (shuffled) {
if (regenShuffledQueue) {
mutableQueue =
parent
.let { parent ->
when (parent) {
null -> library.songs
is Album -> parent.songs
is Artist -> parent.songs
is Genre -> parent.songs
}
}
.toMutableList()
}
mutableQueue.shuffle()
if (keep != null) {
mutableQueue.add(0, mutableQueue.removeAt(mutableQueue.indexOf(keep)))
}
index = 0
} else {
mutableQueue =
parent
.let { parent ->
when (parent) {
null -> settingsManager.libSongSort.songs(library.songs)
is Album -> settingsManager.detailAlbumSort.album(parent)
is Artist -> settingsManager.detailArtistSort.artist(parent)
is Genre -> settingsManager.detailGenreSort.genre(parent)
}
}
.toMutableList()
index = keep?.let(queue::indexOf) ?: 0
}
isShuffled = shuffled
}
// --- STATE FUNCTIONS ---
/** Set whether this instance is currently [playing]. */
fun setPlaying(playing: Boolean) {
if (mIsPlaying != playing) {
if (playing) {
mHasPlayed = true
}
mIsPlaying = playing
for (callback in callbacks) {
callback.onPlayingChanged(playing)
}
}
}
/**
* Update the current [position]. Will not notify listeners of a seek event.
* @param position The new position in millis.
* Update the current [positionMs]. Will not notify listeners of a seek event.
* @param positionMs The new position in millis.
* @see seekTo
*/
fun synchronizePosition(position: Long) {
mSong?.let { song ->
fun synchronizePosition(positionMs: Long) {
// Don't accept any bugged positions that are over the duration of the song.
if (position <= song.duration) {
mPosition = position
val maxDuration = song?.duration ?: -1
if (positionMs <= maxDuration) {
this.positionMs = positionMs
notifyPositionChanged()
}
}
}
/**
* **Seek** to a [position], this calls [PlaybackStateManager.Callback.onSeek] to notify
* **Seek** to a [positionMs], this calls [PlaybackStateManager.Callback.onSeek] to notify
* elements that rely on that.
* @param position The position to seek to in millis.
* @param positionMs The position to seek to in millis.
*/
fun seekTo(position: Long) {
mPosition = position
fun seekTo(positionMs: Long) {
this.positionMs = positionMs
notifySeekEvent()
notifyPositionChanged()
callbacks.forEach { it.onSeek(position) }
}
/** Rewind to the beginning of a song. */
fun rewind() {
seekTo(0)
setPlaying(true)
}
fun rewind() = seekTo(0)
/** Loop playback around to the beginning. */
fun loop() {
seekTo(0)
setPlaying(!settingsManager.pauseOnLoop)
}
fun loop() = seekTo(0)
/** Set the [LoopMode] to [mode]. */
fun setLoopMode(mode: LoopMode) {
mLoopMode = mode
notifyLoopModeChanged()
}
/** Mark whether this instance has played or not */
fun setHasPlayed(hasPlayed: Boolean) {
mHasPlayed = hasPlayed
}
// TODO: Rework these methods eventually
/** Mark this instance as restored. */
fun markRestored() {
mIsRestored = true
isRestored = true
}
// --- PERSISTENCE FUNCTIONS ---
@ -450,15 +337,15 @@ class PlaybackStateManager private constructor() {
database.writeState(
PlaybackStateDatabase.SavedState(
song,
position,
positionMs,
parent,
index,
playbackMode,
isShuffling,
isShuffled,
loopMode,
))
database.writeQueue(mQueue)
database.writeQueue(mutableQueue)
this@PlaybackStateManager.logD(
"State save completed successfully in ${System.currentTimeMillis() - start}ms")
@ -487,11 +374,16 @@ class PlaybackStateManager private constructor() {
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
if (playbackState != null) {
unpackFromPlaybackState(playbackState)
mQueue = queue
notifyQueueChanged()
doParentSanityCheck(playbackState.playbackMode)
doIndexSanityCheck()
parent = playbackState.parent
mutableQueue = queue
index = playbackState.queueIndex
loopMode = playbackState.loopMode
isShuffled = playbackState.isShuffled
notifyNewPlayback()
seekTo(playbackState.positionMs)
notifyLoopModeChanged()
notifyShuffledChanged()
}
logD("State load completed successfully in ${System.currentTimeMillis() - start}ms")
@ -499,105 +391,35 @@ class PlaybackStateManager private constructor() {
markRestored()
}
/** Unpack a [playbackState] into this instance. */
private fun unpackFromPlaybackState(playbackState: PlaybackStateDatabase.SavedState) {
// Do queue setup first
mParent = playbackState.parent
mIndex = playbackState.queueIndex
// Then set up the current state
mSong = playbackState.song
mLoopMode = playbackState.loopMode
mIsShuffling = playbackState.isShuffling
notifySongChanged()
notifyParentChanged()
seekTo(playbackState.position)
notifyShufflingChanged()
notifyLoopModeChanged()
}
/** Do a sanity check to make sure the parent was not lost in the restore process. */
private fun doParentSanityCheck(playbackMode: PlaybackMode) {
// Check if the parent was lost while in the DB.
if (mSong != null && mParent == null && playbackMode != PlaybackMode.ALL_SONGS) {
logD("Parent lost, attempting restore")
mParent =
when (playbackMode) {
PlaybackMode.ALL_SONGS -> null
PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album
PlaybackMode.IN_ARTIST -> mQueue.firstOrNull()?.album?.artist
PlaybackMode.IN_GENRE -> mQueue.firstOrNull()?.genre
}
notifyParentChanged()
}
}
/** Do a sanity check to make sure that the index lines up with the current song. */
private fun doIndexSanityCheck() {
// Be careful with how we handle the queue since a possible index de-sync
// could easily result in an OOB crash.
if (mSong != null && mSong != mQueue.getOrNull(mIndex)) {
val correctedIndex = mQueue.wobblyIndexOfFirst(mIndex, mSong)
if (correctedIndex > -1) {
logD("Correcting malformed index to $correctedIndex")
mIndex = correctedIndex
notifyQueueChanged()
}
}
}
/**
* Finds the index of an item through a sort-of "wobbly" search where it progressively searches
* for item away from the [start] index, instead of from position zero. This is useful, as it
* increases the likelihood that the correct index was found instead of the index of a
* duplicate.
*/
private fun <T> List<T>.wobblyIndexOfFirst(start: Int, item: T): Int {
if (start !in indices) {
return -1
}
var idx = start
var multiplier = -1
var delta = -1
while (true) {
idx += delta
if (idx !in indices) {
if (-idx !in indices) {
return -1
}
} else if (this.getOrNull(idx) == item) {
return idx
}
delta = -delta
multiplier = -multiplier
delta += multiplier
}
}
// --- CALLBACKS ---
private fun notifySongChanged() {
private fun notifyIndexMoved() {
for (callback in callbacks) {
callback.onSongChanged(song)
callback.onIndexMoved(index)
}
}
private fun notifyParentChanged() {
private fun notifyQueueChanged() {
for (callback in callbacks) {
callback.onParentChanged(parent)
callback.onQueueChanged(index, queue)
}
}
private fun notifyNewPlayback() {
for (callback in callbacks) {
callback.onNewPlayback(index, queue, parent)
}
}
private fun notifyPlayingChanged() {
for (callback in callbacks) {
callback.onPlayingChanged(isPlaying)
}
}
private fun notifyPositionChanged() {
for (callback in callbacks) {
callback.onPositionChanged(position)
callback.onPositionChanged(positionMs)
}
}
@ -607,16 +429,15 @@ class PlaybackStateManager private constructor() {
}
}
private fun notifyShufflingChanged() {
private fun notifyShuffledChanged() {
for (callback in callbacks) {
callback.onShuffleChanged(isShuffling)
callback.onShuffledChanged(isShuffled)
}
}
/** Force any callbacks to receive a queue update. */
private fun notifyQueueChanged() {
private fun notifySeekEvent() {
for (callback in callbacks) {
callback.onQueueChanged(mQueue, mIndex)
callback.onSeek(positionMs)
}
}
@ -625,14 +446,16 @@ class PlaybackStateManager private constructor() {
* [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback].
*/
interface Callback {
fun onSongChanged(song: Song?) {}
fun onParentChanged(parent: MusicParent?) {}
fun onPositionChanged(position: Long) {}
fun onQueueChanged(queue: List<Song>, index: Int) {}
fun onIndexMoved(index: Int) {}
fun onQueueChanged(index: Int, queue: List<Song>) {}
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
fun onPlayingChanged(isPlaying: Boolean) {}
fun onShuffleChanged(isShuffling: Boolean) {}
fun onPositionChanged(positionMs: Long) {}
fun onLoopModeChanged(loopMode: LoopMode) {}
fun onSeek(position: Long) {}
fun onShuffledChanged(isShuffled: Boolean) {}
fun onSeek(positionMs: Long) {}
}
companion object {

View file

@ -95,13 +95,13 @@ private constructor(private val context: Context, mediaToken: MediaSessionCompat
}
/** Update the first action to reflect the [loopMode] given. */
fun setLoop(loopMode: LoopMode) {
fun setLoopMode(loopMode: LoopMode) {
mActions[0] = buildLoopAction(context, loopMode)
}
/** Update the first action to reflect whether the queue is shuffled or not */
fun setShuffle(isShuffling: Boolean) {
mActions[0] = buildShuffleAction(context, isShuffling)
fun setShuffled(isShuffled: Boolean) {
mActions[0] = buildShuffleAction(context, isShuffled)
}
/** Apply the current [parent] to the header of the notification. */

View file

@ -73,6 +73,8 @@ import org.oxycblt.auxio.widgets.WidgetProvider
*
* TODO: Move all external exposal from passing around PlaybackStateManager to passing around the
* MediaMetadata instance. Generally makes it easier to encapsulate this class.
*
* TODO: Move hasPlayed to here as well.
*/
class PlaybackService :
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
@ -131,6 +133,7 @@ class PlaybackService :
delay(POS_POLL_INTERVAL)
}
}
// --- SYSTEM SETUP ---
widgets = WidgetController(this)
@ -161,9 +164,7 @@ class PlaybackService :
// --- PLAYBACKSTATEMANAGER SETUP ---
playbackManager.setHasPlayed(playbackManager.isPlaying)
playbackManager.addCallback(this)
if (playbackManager.song != null || playbackManager.isRestored) {
restore()
}
@ -190,7 +191,7 @@ class PlaybackService :
settingsManager.removeCallback(this)
// Pause just in case this destruction was unexpected.
playbackManager.setPlaying(false)
playbackManager.isPlaying = false
// The service coroutines last job is to save the state to the DB, before terminating itself
// FIXME: This is a terrible idea, move this to when the user closes the notification
@ -207,7 +208,7 @@ class PlaybackService :
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (playbackManager.isPlaying != playWhenReady) {
playbackManager.setPlaying(playWhenReady)
playbackManager.isPlaying = playWhenReady
}
}
@ -234,10 +235,8 @@ class PlaybackService :
newPosition: Player.PositionInfo,
reason: Int
) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
playbackManager.synchronizePosition(player.currentPosition)
}
}
override fun onTracksInfoChanged(tracksInfo: TracksInfo) {
super.onTracksInfoChanged(tracksInfo)
@ -258,7 +257,15 @@ class PlaybackService :
// --- PLAYBACK STATE CALLBACK OVERRIDES ---
override fun onSongChanged(song: Song?) {
override fun onIndexMoved(index: Int) {
onSongChanged(playbackManager.song)
}
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
onSongChanged(playbackManager.song)
}
private fun onSongChanged(song: Song?) {
if (song != null) {
logD("Setting player to ${song.rawName}")
player.setMediaItem(MediaItem.fromUri(song.uri))
@ -273,11 +280,6 @@ class PlaybackService :
stopForegroundAndNotification()
}
override fun onParentChanged(parent: MusicParent?) {
notification.setParent(parent)
startForegroundOrNotify()
}
override fun onPlayingChanged(isPlaying: Boolean) {
player.playWhenReady = isPlaying
notification.setPlaying(isPlaying)
@ -286,20 +288,20 @@ class PlaybackService :
override fun onLoopModeChanged(loopMode: LoopMode) {
if (!settingsManager.useAltNotifAction) {
notification.setLoop(loopMode)
notification.setLoopMode(loopMode)
startForegroundOrNotify()
}
}
override fun onShuffleChanged(isShuffling: Boolean) {
override fun onShuffledChanged(isShuffled: Boolean) {
if (settingsManager.useAltNotifAction) {
notification.setShuffle(isShuffling)
notification.setShuffled(isShuffled)
startForegroundOrNotify()
}
}
override fun onSeek(position: Long) {
player.seekTo(position)
override fun onSeek(positionMs: Long) {
player.seekTo(positionMs)
}
// --- SETTINGSMANAGER OVERRIDES ---
@ -313,9 +315,9 @@ class PlaybackService :
override fun onNotifActionUpdate(useAltAction: Boolean) {
if (useAltAction) {
notification.setShuffle(playbackManager.isShuffling)
notification.setShuffled(playbackManager.isShuffled)
} else {
notification.setLoop(playbackManager.loopMode)
notification.setLoopMode(playbackManager.loopMode)
}
startForegroundOrNotify()
@ -375,13 +377,10 @@ class PlaybackService :
private fun restore() {
logD("Restoring the service state")
// Re-call existing callbacks with the current values to restore everything
onParentChanged(playbackManager.parent)
onPlayingChanged(playbackManager.isPlaying)
onShuffleChanged(playbackManager.isShuffling)
onLoopModeChanged(playbackManager.loopMode)
onSongChanged(playbackManager.song)
onSeek(playbackManager.position)
onSeek(playbackManager.positionMs)
onShuffledChanged(playbackManager.isShuffled)
onLoopModeChanged(playbackManager.loopMode)
// Notify other classes that rely on this service to also update.
widgets.update()
@ -391,7 +390,7 @@ class PlaybackService :
* Bring the service into the foreground and show the notification, or refresh the notification.
*/
private fun startForegroundOrNotify() {
if (playbackManager.hasPlayed && playbackManager.song != null) {
if (/*playbackManager.hasPlayed &&*/ playbackManager.song != null) {
logD("Starting foreground/notifying")
if (!isForeground) {
@ -450,14 +449,13 @@ class PlaybackService :
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
// --- AUXIO EVENTS ---
ACTION_PLAY_PAUSE -> playbackManager.setPlaying(!playbackManager.isPlaying)
ACTION_LOOP -> playbackManager.setLoopMode(playbackManager.loopMode.increment())
ACTION_SHUFFLE ->
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
ACTION_PLAY_PAUSE -> playbackManager.isPlaying = !playbackManager.isPlaying
ACTION_LOOP -> playbackManager.loopMode = playbackManager.loopMode.increment()
ACTION_SHUFFLE -> playbackManager.reshuffle(!playbackManager.isShuffled)
ACTION_SKIP_PREV -> playbackManager.prev()
ACTION_SKIP_NEXT -> playbackManager.next()
ACTION_EXIT -> {
playbackManager.setPlaying(false)
playbackManager.isPlaying = false
stopForegroundAndNotification()
}
WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update()
@ -478,7 +476,7 @@ class PlaybackService :
settingsManager.headsetAutoplay &&
initialHeadsetPlugEventHandled) {
logD("Device connected, resuming")
playbackManager.setPlaying(true)
playbackManager.isPlaying = true
}
}
@ -486,7 +484,7 @@ class PlaybackService :
private fun pauseFromPlug() {
if (playbackManager.song != null) {
logD("Device disconnected, pausing")
playbackManager.setPlaying(false)
playbackManager.isPlaying = false
}
}
}

View file

@ -25,6 +25,7 @@ import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import com.google.android.exoplayer2.Player
import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager
@ -59,11 +60,11 @@ class PlaybackSessionConnector(
// --- MEDIASESSION CALLBACKS ---
override fun onPlay() {
playbackManager.setPlaying(true)
playbackManager.isPlaying = true
}
override fun onPause() {
playbackManager.setPlaying(false)
playbackManager.isPlaying = false
}
override fun onSkipToNext() {
@ -80,25 +81,23 @@ class PlaybackSessionConnector(
override fun onRewind() {
playbackManager.rewind()
playbackManager.isPlaying = true
}
override fun onSetRepeatMode(repeatMode: Int) {
val mode =
playbackManager.loopMode =
when (repeatMode) {
PlaybackStateCompat.REPEAT_MODE_ALL -> LoopMode.ALL
PlaybackStateCompat.REPEAT_MODE_GROUP -> LoopMode.ALL
PlaybackStateCompat.REPEAT_MODE_ONE -> LoopMode.TRACK
else -> LoopMode.NONE
}
playbackManager.setLoopMode(mode)
}
override fun onSetShuffleMode(shuffleMode: Int) {
playbackManager.setShuffling(
playbackManager.reshuffle(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
true)
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP)
}
override fun onStop() {
@ -108,7 +107,19 @@ class PlaybackSessionConnector(
// --- PLAYBACKSTATEMANAGER CALLBACKS ---
override fun onSongChanged(song: Song?) {
override fun onIndexMoved(index: Int) {
onSongChanged(playbackManager.song)
}
override fun onQueueChanged(index: Int, queue: List<Song>) {
onSongChanged(playbackManager.song)
}
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
onSongChanged(playbackManager.song)
}
fun onSongChanged(song: Song?) {
if (song == null) {
mediaSession.setMetadata(emptyMetadata)
return

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.widgets
import android.content.Context
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager
@ -61,7 +62,15 @@ class WidgetController(private val context: Context) :
// --- PLAYBACKSTATEMANAGER CALLBACKS ---
override fun onSongChanged(song: Song?) {
override fun onIndexMoved(index: Int) {
widget.update(context, playbackManager)
}
override fun onQueueChanged(index: Int, queue: List<Song>) {
widget.update(context, playbackManager)
}
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
widget.update(context, playbackManager)
}
@ -69,7 +78,7 @@ class WidgetController(private val context: Context) :
widget.update(context, playbackManager)
}
override fun onShuffleChanged(isShuffling: Boolean) {
override fun onShuffledChanged(isShuffled: Boolean) {
widget.update(context, playbackManager)
}

View file

@ -73,7 +73,7 @@ class WidgetProvider : AppWidgetProvider() {
song,
bitmap,
playbackManager.isPlaying,
playbackManager.isShuffling,
playbackManager.isShuffled,
playbackManager.loopMode)
// Map each widget form to the cells where it would look at least okay.