diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index 460d4c132..b0cb9d25b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -80,7 +80,7 @@ class PlaybackBarFragment : ViewBindingFragment() { 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() { 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?) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 70b573391..47f117303 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -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 } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 5dc1c3967..9885ebba4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -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()) - 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 get() = mIsPlaying - val isShuffling: LiveData - get() = mIsShuffling + /** The current playback position, in seconds */ + val positionSecs: LiveData + get() = mPositionSecs /** The current repeat mode, see [LoopMode] for more information */ val loopMode: LiveData get() = mLoopMode - /** The current playback position, in seconds */ - val positionSeconds: LiveData - get() = mPositionSeconds + val isShuffled: LiveData + get() = mIsShuffled /** The queue, without the previous items. */ val nextUp: LiveData> get() = mNextUp - /** The current [PlaybackMode] that also determines the queue */ - val playbackMode: LiveData - 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, index: Int) { + override fun onQueueChanged(index: Int, queue: List) { + mSong.value = playbackManager.song mNextUp.value = queue.slice(index.inc() until queue.size) } + override fun onNewPlayback(index: Int, queue: List, 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt index b3d7cf2ba..17aac32e7 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt @@ -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" } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index cc5d7face..fa4acda77 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -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() - 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() /** 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 - 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() + parent = + when (playbackMode) { + PlaybackMode.ALL_SONGS -> null + PlaybackMode.IN_ALBUM -> song.album + 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() - - 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) { - 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) { - 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 song = song ?: return + applyNewQueue(library, shuffled, song, false) + notifyQueueChanged() + notifyShuffledChanged() + } - 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() + 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() } - if (keepSong) { - mIndex = mQueue.indexOf(lastSong) + 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 } - notifyQueueChanged() + 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 -> - // Don't accept any bugged positions that are over the duration of the song. - if (position <= song.duration) { - mPosition = position - notifyPositionChanged() - } + fun synchronizePosition(positionMs: Long) { + // Don't accept any bugged positions that are over the duration of the song. + 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 List.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, index: Int) {} + fun onIndexMoved(index: Int) {} + fun onQueueChanged(index: Int, queue: List) {} + fun onNewPlayback(index: Int, queue: List, 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 { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt index 5f30e90cd..7bc010ed8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt @@ -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. */ diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index d40825258..bfb17c831 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -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,9 +235,7 @@ class PlaybackService : newPosition: Player.PositionInfo, reason: Int ) { - if (reason == Player.DISCONTINUITY_REASON_SEEK) { - playbackManager.synchronizePosition(player.currentPosition) - } + playbackManager.synchronizePosition(player.currentPosition) } override fun onTracksInfoChanged(tracksInfo: 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, 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 } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt index cd63183c0..91a5045ee 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt @@ -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) { + onSongChanged(playbackManager.song) + } + + override fun onNewPlayback(index: Int, queue: List, parent: MusicParent?) { + onSongChanged(playbackManager.song) + } + + fun onSongChanged(song: Song?) { if (song == null) { mediaSession.setMetadata(emptyMetadata) return diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt index 0c3eab6a1..591663961 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt @@ -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) { + widget.update(context, playbackManager) + } + + override fun onNewPlayback(index: Int, queue: List, 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) } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 53b4ccd9f..ffe571906 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -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.