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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.playback.state package org.oxycblt.auxio.playback.state
import android.content.Context import android.content.Context
import kotlin.math.max
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.music.Album 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.music.Song
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
/** /**
* Master class (and possible god object) for the playback state. * Master class (and possible god object) for the playback state.
@ -47,54 +47,43 @@ class PlaybackStateManager private constructor() {
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
// Playback // Playback
private var mSong: Song? = null private var mutableQueue = mutableListOf<Song>()
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
/** The currently playing song. Null if there isn't one */ /** The currently playing song. Null if there isn't one */
val song: Song? val song
get() = mSong get() = queue.getOrNull(index)
/** The parent the queue is based on, null if all_songs */ /** The parent the queue is based on, null if all songs */
val parent: MusicParent? var parent: MusicParent? = null
get() = mParent private set
/** The current queue determined by [parent] */ /** The current queue determined by [parent] */
val queue: List<Song> val queue
get() = mQueue get() = mutableQueue
/** The current position in the queue */ /** The current position in the queue */
val index: Int var index = -1
get() = mIndex private set
/** Whether playback is paused or not */ /** Whether playback is playing or not */
val isPlaying: Boolean var isPlaying = false
get() = mIsPlaying set(value) {
field = value
notifyPlayingChanged()
}
/** The current playback progress */ /** The current playback progress */
val position: Long var positionMs = 0L
get() = mPosition private set
/** The current [LoopMode] */ /** The current [LoopMode] */
val loopMode: LoopMode var loopMode = LoopMode.NONE
get() = mLoopMode set(value) {
field = value
notifyLoopModeChanged()
}
/** Whether the queue is shuffled */ /** Whether the queue is shuffled */
val isShuffling: Boolean var isShuffled = false
get() = mIsShuffling private set
/** Whether this instance has already been restored */ /** Whether this instance has already been restored */
val isRestored: Boolean var isRestored = false
get() = mIsRestored private set
/** Whether playback has begun in this instance during **PlaybackService's Lifecycle.** */
val hasPlayed: Boolean
get() = mHasPlayed
// --- CALLBACKS --- // --- CALLBACKS ---
@ -117,84 +106,46 @@ class PlaybackStateManager private constructor() {
/** /**
* Play a [song]. * 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) { fun play(song: Song, playbackMode: PlaybackMode) {
logD("Updating song to ${song.rawName} and mode to $mode") val library = musicStore.library ?: return
when (mode) { parent =
PlaybackMode.ALL_SONGS -> { when (playbackMode) {
val musicStore = musicStore.library ?: return PlaybackMode.ALL_SONGS -> null
mParent = null PlaybackMode.IN_ALBUM -> song.album
mQueue = musicStore.songs.toMutableList() PlaybackMode.IN_ARTIST -> song.album.artist
} PlaybackMode.IN_GENRE -> song.genre
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()
}
} }
notifyParentChanged() applyNewQueue(library, settingsManager.keepShuffle && isShuffled, song, true)
notifyNewPlayback()
updatePlayback(song) notifyShuffledChanged()
// Keep shuffle on, if enabled isPlaying = true
setShuffling(settingsManager.keepShuffle && isShuffling, keepSong = true)
} }
/** /**
* Play a [parent], such as an artist or album. * Play a [parent], such as an artist or album.
* @param shuffled Whether the queue is shuffled or not * @param shuffled Whether the queue is shuffled or not
*/ */
fun playParent(parent: MusicParent, shuffled: Boolean) { fun play(parent: MusicParent?, shuffled: Boolean) {
logD("Playing ${parent.rawName}") val library = musicStore.library ?: return
this.parent = parent
mParent = parent applyNewQueue(library, shuffled, null, true)
notifyParentChanged() notifyNewPlayback()
mIndex = 0 notifyShuffledChanged()
isPlaying = true
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])
} }
/** Shuffle all songs. */ /** Shuffle all songs. */
fun shuffleAll() { fun shuffleAll() {
val library = musicStore.library ?: return val library = musicStore.library ?: return
parent = null
mQueue = library.songs.toMutableList() applyNewQueue(library, true, null, true)
mParent = null notifyNewPlayback()
notifyShuffledChanged()
setShuffling(true, keepSong = false) isPlaying = true
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)
} }
// --- QUEUE FUNCTIONS --- // --- QUEUE FUNCTIONS ---
@ -203,226 +154,162 @@ class PlaybackStateManager private constructor() {
fun next() { fun next() {
// Increment the index, if it cannot be incremented any further, then // Increment the index, if it cannot be incremented any further, then
// loop and pause/resume playback depending on the setting // loop and pause/resume playback depending on the setting
if (mIndex < mQueue.lastIndex) { if (index < mutableQueue.lastIndex) {
mIndex = mIndex.inc() goto(++index, true)
updatePlayback(mQueue[mIndex])
} else { } else {
mIndex = 0 goto(0, loopMode == LoopMode.ALL)
updatePlayback(mQueue[mIndex], shouldPlay = loopMode == LoopMode.ALL)
} }
notifyQueueChanged()
} }
/** Go to the previous song, doing any checks that are needed. */ /** Go to the previous song, doing any checks that are needed. */
fun prev() { fun prev() {
// 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 (settingsManager.rewindWithPrev && mPosition >= REWIND_THRESHOLD) { if (settingsManager.rewindWithPrev && positionMs >= REWIND_THRESHOLD) {
rewind() rewind()
isPlaying = true
} else { } else {
// Only decrement the index if there's a song to move back to goto(max(--index, 0), true)
if (mIndex > 0) {
mIndex = mIndex.dec()
}
updatePlayback(mQueue[mIndex])
notifyQueueChanged()
} }
} }
// --- QUEUE EDITING FUNCTIONS --- private fun goto(idx: Int, play: Boolean) {
index = idx
/** Remove a queue item at [index]. Will ignore invalid indexes. */ notifyIndexMoved()
fun removeQueueItem(index: Int): Boolean { isPlaying = play
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
} }
/** Add a [song] to the top of the queue. */ /** Add a [song] to the top of the queue. */
fun playNext(song: Song) { fun playNext(song: Song) {
if (mQueue.isEmpty()) { mutableQueue.add(++index, song)
return
}
mQueue.add(mIndex + 1, song)
notifyQueueChanged() notifyQueueChanged()
} }
/** Add a list of [songs] to the top of the queue. */ /** Add a list of [songs] to the top of the queue. */
fun playNext(songs: List<Song>) { fun playNext(songs: List<Song>) {
if (mQueue.isEmpty()) { mutableQueue.addAll(++index, songs)
return
}
mQueue.addAll(mIndex + 1, songs)
notifyQueueChanged() notifyQueueChanged()
} }
/** Add a [song] to the end of the queue. */ /** Add a [song] to the end of the queue. */
fun addToQueue(song: Song) { fun addToQueue(song: Song) {
mQueue.add(song) mutableQueue.add(song)
notifyQueueChanged() notifyQueueChanged()
} }
/** Add a list of [songs] to the end of the queue. */ /** Add a list of [songs] to the end of the queue. */
fun addToQueue(songs: List<Song>) { fun addToQueue(songs: List<Song>) {
mQueue.addAll(songs) mutableQueue.addAll(songs)
notifyQueueChanged() notifyQueueChanged()
} }
// --- SHUFFLE FUNCTIONS --- /** 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")
* Set whether this instance is [shuffled]. Updates the queue accordingly. mutableQueue.add(to, mutableQueue.removeAt(from))
* @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]
}
notifyQueueChanged() notifyQueueChanged()
} }
/** /** Remove a queue item at [index]. Will ignore invalid indexes. */
* Reset the queue to its normal, ordered state. fun removeQueueItem(index: Int) {
* @param keepSong Whether the current song should be kept as the queue is un-shuffled logD("Removing item ${mutableQueue[index].rawName}")
*/ mutableQueue.removeAt(index)
private fun resetShuffle(keepSong: Boolean) { notifyQueueChanged()
}
/** Set whether this instance is [shuffled]. Updates the queue accordingly. */
fun reshuffle(shuffled: Boolean) {
val library = musicStore.library ?: return val library = musicStore.library ?: return
val lastSong = mSong val song = song ?: return
applyNewQueue(library, shuffled, song, false)
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)
}
notifyQueueChanged() 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 --- // --- 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. * Update the current [positionMs]. Will not notify listeners of a seek event.
* @param position The new position in millis. * @param positionMs The new position in millis.
* @see seekTo * @see seekTo
*/ */
fun synchronizePosition(position: Long) { fun synchronizePosition(positionMs: Long) {
mSong?.let { song ->
// Don't accept any bugged positions that are over the duration of the song. // Don't accept any bugged positions that are over the duration of the song.
if (position <= song.duration) { val maxDuration = song?.duration ?: -1
mPosition = position if (positionMs <= maxDuration) {
this.positionMs = positionMs
notifyPositionChanged() 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. * 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) { fun seekTo(positionMs: Long) {
mPosition = position this.positionMs = positionMs
notifySeekEvent()
notifyPositionChanged() notifyPositionChanged()
callbacks.forEach { it.onSeek(position) }
} }
/** Rewind to the beginning of a song. */ /** Rewind to the beginning of a song. */
fun rewind() { fun rewind() = seekTo(0)
seekTo(0)
setPlaying(true)
}
/** Loop playback around to the beginning. */ /** Loop playback around to the beginning. */
fun loop() { fun loop() = seekTo(0)
seekTo(0)
setPlaying(!settingsManager.pauseOnLoop)
}
/** Set the [LoopMode] to [mode]. */ // TODO: Rework these methods eventually
fun setLoopMode(mode: LoopMode) {
mLoopMode = mode
notifyLoopModeChanged()
}
/** Mark whether this instance has played or not */
fun setHasPlayed(hasPlayed: Boolean) {
mHasPlayed = hasPlayed
}
/** Mark this instance as restored. */ /** Mark this instance as restored. */
fun markRestored() { fun markRestored() {
mIsRestored = true isRestored = true
} }
// --- PERSISTENCE FUNCTIONS --- // --- PERSISTENCE FUNCTIONS ---
@ -450,15 +337,15 @@ class PlaybackStateManager private constructor() {
database.writeState( database.writeState(
PlaybackStateDatabase.SavedState( PlaybackStateDatabase.SavedState(
song, song,
position, positionMs,
parent, parent,
index, index,
playbackMode, playbackMode,
isShuffling, isShuffled,
loopMode, loopMode,
)) ))
database.writeQueue(mQueue) database.writeQueue(mutableQueue)
this@PlaybackStateManager.logD( this@PlaybackStateManager.logD(
"State save completed successfully in ${System.currentTimeMillis() - start}ms") "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 // Get off the IO coroutine since it will cause LiveData updates to throw an exception
if (playbackState != null) { if (playbackState != null) {
unpackFromPlaybackState(playbackState) parent = playbackState.parent
mQueue = queue mutableQueue = queue
notifyQueueChanged() index = playbackState.queueIndex
doParentSanityCheck(playbackState.playbackMode) loopMode = playbackState.loopMode
doIndexSanityCheck() isShuffled = playbackState.isShuffled
notifyNewPlayback()
seekTo(playbackState.positionMs)
notifyLoopModeChanged()
notifyShuffledChanged()
} }
logD("State load completed successfully in ${System.currentTimeMillis() - start}ms") logD("State load completed successfully in ${System.currentTimeMillis() - start}ms")
@ -499,105 +391,35 @@ class PlaybackStateManager private constructor() {
markRestored() 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 --- // --- CALLBACKS ---
private fun notifySongChanged() { private fun notifyIndexMoved() {
for (callback in callbacks) { for (callback in callbacks) {
callback.onSongChanged(song) callback.onIndexMoved(index)
} }
} }
private fun notifyParentChanged() { private fun notifyQueueChanged() {
for (callback in callbacks) { 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() { private fun notifyPositionChanged() {
for (callback in callbacks) { 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) { for (callback in callbacks) {
callback.onShuffleChanged(isShuffling) callback.onShuffledChanged(isShuffled)
} }
} }
/** Force any callbacks to receive a queue update. */ private fun notifySeekEvent() {
private fun notifyQueueChanged() {
for (callback in callbacks) { 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]. * [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback].
*/ */
interface Callback { interface Callback {
fun onSongChanged(song: Song?) {} fun onIndexMoved(index: Int) {}
fun onParentChanged(parent: MusicParent?) {} fun onQueueChanged(index: Int, queue: List<Song>) {}
fun onPositionChanged(position: Long) {} fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
fun onQueueChanged(queue: List<Song>, index: Int) {}
fun onPlayingChanged(isPlaying: Boolean) {} fun onPlayingChanged(isPlaying: Boolean) {}
fun onShuffleChanged(isShuffling: Boolean) {} fun onPositionChanged(positionMs: Long) {}
fun onLoopModeChanged(loopMode: LoopMode) {} fun onLoopModeChanged(loopMode: LoopMode) {}
fun onSeek(position: Long) {} fun onShuffledChanged(isShuffled: Boolean) {}
fun onSeek(positionMs: Long) {}
} }
companion object { 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. */ /** Update the first action to reflect the [loopMode] given. */
fun setLoop(loopMode: LoopMode) { fun setLoopMode(loopMode: LoopMode) {
mActions[0] = buildLoopAction(context, loopMode) mActions[0] = buildLoopAction(context, loopMode)
} }
/** Update the first action to reflect whether the queue is shuffled or not */ /** Update the first action to reflect whether the queue is shuffled or not */
fun setShuffle(isShuffling: Boolean) { fun setShuffled(isShuffled: Boolean) {
mActions[0] = buildShuffleAction(context, isShuffling) mActions[0] = buildShuffleAction(context, isShuffled)
} }
/** Apply the current [parent] to the header of the notification. */ /** 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 * TODO: Move all external exposal from passing around PlaybackStateManager to passing around the
* MediaMetadata instance. Generally makes it easier to encapsulate this class. * MediaMetadata instance. Generally makes it easier to encapsulate this class.
*
* TODO: Move hasPlayed to here as well.
*/ */
class PlaybackService : class PlaybackService :
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback { Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
@ -131,6 +133,7 @@ class PlaybackService :
delay(POS_POLL_INTERVAL) delay(POS_POLL_INTERVAL)
} }
} }
// --- SYSTEM SETUP --- // --- SYSTEM SETUP ---
widgets = WidgetController(this) widgets = WidgetController(this)
@ -161,9 +164,7 @@ class PlaybackService :
// --- PLAYBACKSTATEMANAGER SETUP --- // --- PLAYBACKSTATEMANAGER SETUP ---
playbackManager.setHasPlayed(playbackManager.isPlaying)
playbackManager.addCallback(this) playbackManager.addCallback(this)
if (playbackManager.song != null || playbackManager.isRestored) { if (playbackManager.song != null || playbackManager.isRestored) {
restore() restore()
} }
@ -190,7 +191,7 @@ class PlaybackService :
settingsManager.removeCallback(this) settingsManager.removeCallback(this)
// Pause just in case this destruction was unexpected. // 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 // 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 // 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) { override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason) super.onPlayWhenReadyChanged(playWhenReady, reason)
if (playbackManager.isPlaying != playWhenReady) { if (playbackManager.isPlaying != playWhenReady) {
playbackManager.setPlaying(playWhenReady) playbackManager.isPlaying = playWhenReady
} }
} }
@ -234,10 +235,8 @@ class PlaybackService :
newPosition: Player.PositionInfo, newPosition: Player.PositionInfo,
reason: Int reason: Int
) { ) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
playbackManager.synchronizePosition(player.currentPosition) playbackManager.synchronizePosition(player.currentPosition)
} }
}
override fun onTracksInfoChanged(tracksInfo: TracksInfo) { override fun onTracksInfoChanged(tracksInfo: TracksInfo) {
super.onTracksInfoChanged(tracksInfo) super.onTracksInfoChanged(tracksInfo)
@ -258,7 +257,15 @@ class PlaybackService :
// --- PLAYBACK STATE CALLBACK OVERRIDES --- // --- 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) { if (song != null) {
logD("Setting player to ${song.rawName}") logD("Setting player to ${song.rawName}")
player.setMediaItem(MediaItem.fromUri(song.uri)) player.setMediaItem(MediaItem.fromUri(song.uri))
@ -273,11 +280,6 @@ class PlaybackService :
stopForegroundAndNotification() stopForegroundAndNotification()
} }
override fun onParentChanged(parent: MusicParent?) {
notification.setParent(parent)
startForegroundOrNotify()
}
override fun onPlayingChanged(isPlaying: Boolean) { override fun onPlayingChanged(isPlaying: Boolean) {
player.playWhenReady = isPlaying player.playWhenReady = isPlaying
notification.setPlaying(isPlaying) notification.setPlaying(isPlaying)
@ -286,20 +288,20 @@ class PlaybackService :
override fun onLoopModeChanged(loopMode: LoopMode) { override fun onLoopModeChanged(loopMode: LoopMode) {
if (!settingsManager.useAltNotifAction) { if (!settingsManager.useAltNotifAction) {
notification.setLoop(loopMode) notification.setLoopMode(loopMode)
startForegroundOrNotify() startForegroundOrNotify()
} }
} }
override fun onShuffleChanged(isShuffling: Boolean) { override fun onShuffledChanged(isShuffled: Boolean) {
if (settingsManager.useAltNotifAction) { if (settingsManager.useAltNotifAction) {
notification.setShuffle(isShuffling) notification.setShuffled(isShuffled)
startForegroundOrNotify() startForegroundOrNotify()
} }
} }
override fun onSeek(position: Long) { override fun onSeek(positionMs: Long) {
player.seekTo(position) player.seekTo(positionMs)
} }
// --- SETTINGSMANAGER OVERRIDES --- // --- SETTINGSMANAGER OVERRIDES ---
@ -313,9 +315,9 @@ class PlaybackService :
override fun onNotifActionUpdate(useAltAction: Boolean) { override fun onNotifActionUpdate(useAltAction: Boolean) {
if (useAltAction) { if (useAltAction) {
notification.setShuffle(playbackManager.isShuffling) notification.setShuffled(playbackManager.isShuffled)
} else { } else {
notification.setLoop(playbackManager.loopMode) notification.setLoopMode(playbackManager.loopMode)
} }
startForegroundOrNotify() startForegroundOrNotify()
@ -375,13 +377,10 @@ class PlaybackService :
private fun restore() { private fun restore() {
logD("Restoring the service state") 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) 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. // Notify other classes that rely on this service to also update.
widgets.update() widgets.update()
@ -391,7 +390,7 @@ class PlaybackService :
* Bring the service into the foreground and show the notification, or refresh the notification. * Bring the service into the foreground and show the notification, or refresh the notification.
*/ */
private fun startForegroundOrNotify() { private fun startForegroundOrNotify() {
if (playbackManager.hasPlayed && playbackManager.song != null) { if (/*playbackManager.hasPlayed &&*/ playbackManager.song != null) {
logD("Starting foreground/notifying") logD("Starting foreground/notifying")
if (!isForeground) { if (!isForeground) {
@ -450,14 +449,13 @@ class PlaybackService :
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug() AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
// --- AUXIO EVENTS --- // --- AUXIO EVENTS ---
ACTION_PLAY_PAUSE -> playbackManager.setPlaying(!playbackManager.isPlaying) ACTION_PLAY_PAUSE -> playbackManager.isPlaying = !playbackManager.isPlaying
ACTION_LOOP -> playbackManager.setLoopMode(playbackManager.loopMode.increment()) ACTION_LOOP -> playbackManager.loopMode = playbackManager.loopMode.increment()
ACTION_SHUFFLE -> ACTION_SHUFFLE -> playbackManager.reshuffle(!playbackManager.isShuffled)
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
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.setPlaying(false) playbackManager.isPlaying = false
stopForegroundAndNotification() stopForegroundAndNotification()
} }
WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update() WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update()
@ -478,7 +476,7 @@ class PlaybackService :
settingsManager.headsetAutoplay && settingsManager.headsetAutoplay &&
initialHeadsetPlugEventHandled) { initialHeadsetPlugEventHandled) {
logD("Device connected, resuming") logD("Device connected, resuming")
playbackManager.setPlaying(true) playbackManager.isPlaying = true
} }
} }
@ -486,7 +484,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.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 android.support.v4.media.session.PlaybackStateCompat
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import org.oxycblt.auxio.coil.loadBitmap import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
@ -59,11 +60,11 @@ class PlaybackSessionConnector(
// --- MEDIASESSION CALLBACKS --- // --- MEDIASESSION CALLBACKS ---
override fun onPlay() { override fun onPlay() {
playbackManager.setPlaying(true) playbackManager.isPlaying = true
} }
override fun onPause() { override fun onPause() {
playbackManager.setPlaying(false) playbackManager.isPlaying = false
} }
override fun onSkipToNext() { override fun onSkipToNext() {
@ -80,25 +81,23 @@ class PlaybackSessionConnector(
override fun onRewind() { override fun onRewind() {
playbackManager.rewind() playbackManager.rewind()
playbackManager.isPlaying = true
} }
override fun onSetRepeatMode(repeatMode: Int) { override fun onSetRepeatMode(repeatMode: Int) {
val mode = playbackManager.loopMode =
when (repeatMode) { when (repeatMode) {
PlaybackStateCompat.REPEAT_MODE_ALL -> LoopMode.ALL PlaybackStateCompat.REPEAT_MODE_ALL -> LoopMode.ALL
PlaybackStateCompat.REPEAT_MODE_GROUP -> LoopMode.ALL PlaybackStateCompat.REPEAT_MODE_GROUP -> LoopMode.ALL
PlaybackStateCompat.REPEAT_MODE_ONE -> LoopMode.TRACK PlaybackStateCompat.REPEAT_MODE_ONE -> LoopMode.TRACK
else -> LoopMode.NONE else -> LoopMode.NONE
} }
playbackManager.setLoopMode(mode)
} }
override fun onSetShuffleMode(shuffleMode: Int) { override fun onSetShuffleMode(shuffleMode: Int) {
playbackManager.setShuffling( playbackManager.reshuffle(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP, shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP)
true)
} }
override fun onStop() { override fun onStop() {
@ -108,7 +107,19 @@ class PlaybackSessionConnector(
// --- PLAYBACKSTATEMANAGER CALLBACKS --- // --- 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) { if (song == null) {
mediaSession.setMetadata(emptyMetadata) mediaSession.setMetadata(emptyMetadata)
return return

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.widgets package org.oxycblt.auxio.widgets
import android.content.Context import android.content.Context
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
@ -61,7 +62,15 @@ class WidgetController(private val context: Context) :
// --- PLAYBACKSTATEMANAGER CALLBACKS --- // --- 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) widget.update(context, playbackManager)
} }
@ -69,7 +78,7 @@ class WidgetController(private val context: Context) :
widget.update(context, playbackManager) widget.update(context, playbackManager)
} }
override fun onShuffleChanged(isShuffling: Boolean) { override fun onShuffledChanged(isShuffled: Boolean) {
widget.update(context, playbackManager) widget.update(context, playbackManager)
} }

View file

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