From 42325c456e30a298bfb9c0906e0232505683d3d8 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Mon, 26 Oct 2020 09:39:45 -0600 Subject: [PATCH] Refactor playback management Move playback management from PlaybackViewModel to a dedicated class, update naming of playback functions heavily. --- .../java/org/oxycblt/auxio/MainFragment.kt | 2 +- .../auxio/detail/AlbumDetailFragment.kt | 10 +- .../auxio/detail/ArtistDetailFragment.kt | 6 +- .../auxio/detail/GenreDetailFragment.kt | 6 +- .../oxycblt/auxio/library/LibraryFragment.kt | 6 +- .../auxio/playback/CompactPlaybackFragment.kt | 8 +- .../auxio/playback/PlaybackFragment.kt | 12 +- .../oxycblt/auxio/playback/PlaybackService.kt | 25 +- .../auxio/playback/PlaybackViewModel.kt | 428 +++++------------- .../auxio/playback/queue/QueueFragment.kt | 6 +- .../playback/{ => state}/PlaybackMode.kt | 2 +- .../playback/state/PlaybackStateCallback.kt | 12 + .../playback/state/PlaybackStateManager.kt | 344 ++++++++++++++ .../org/oxycblt/auxio/songs/SongsFragment.kt | 4 +- 14 files changed, 523 insertions(+), 348 deletions(-) rename app/src/main/java/org/oxycblt/auxio/playback/{ => state}/PlaybackMode.kt (86%) create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 8ed3b495c..02f8f9a51 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -92,7 +92,7 @@ class MainFragment : Fragment() { // --- VIEWMODEL SETUP --- // Change CompactPlaybackFragment's visibility here so that an animation occurs. - playbackModel.currentSong.observe(viewLifecycleOwner) { + playbackModel.song.observe(viewLifecycleOwner) { if (it == null) { Log.d( this::class.simpleName, diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 8dde340d8..a0d3aa9a5 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -13,7 +13,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding import org.oxycblt.auxio.detail.adapters.DetailSongAdapter import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.playback.PlaybackMode +import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.disable @@ -47,7 +47,7 @@ class AlbumDetailFragment : Fragment() { } val songAdapter = DetailSongAdapter { - playbackModel.update(it, PlaybackMode.IN_ALBUM) + playbackModel.playSong(it, PlaybackMode.IN_ALBUM) } // --- UI SETUP --- @@ -64,11 +64,13 @@ class AlbumDetailFragment : Fragment() { setOnMenuItemClickListener { when (it.itemId) { - R.id.action_shuffle -> playbackModel.play( + R.id.action_shuffle -> playbackModel.playAlbum( detailModel.currentAlbum.value!!, true ) - R.id.action_play -> playbackModel.play(detailModel.currentAlbum.value!!, false) + R.id.action_play -> playbackModel.playAlbum( + detailModel.currentAlbum.value!!, false + ) } true diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 613183667..be6f26c5e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -67,11 +67,13 @@ class ArtistDetailFragment : Fragment() { setOnMenuItemClickListener { when (it.itemId) { - R.id.action_shuffle -> playbackModel.play( + R.id.action_shuffle -> playbackModel.playArtist( detailModel.currentArtist.value!!, true ) - R.id.action_play -> playbackModel.play(detailModel.currentArtist.value!!, false) + R.id.action_play -> playbackModel.playArtist( + detailModel.currentArtist.value!!, false + ) } true diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index b521998a5..e3c528796 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -67,11 +67,13 @@ class GenreDetailFragment : Fragment() { setOnMenuItemClickListener { when (it.itemId) { - R.id.action_shuffle -> playbackModel.play( + R.id.action_shuffle -> playbackModel.playGenre( detailModel.currentGenre.value!!, true ) - R.id.action_play -> playbackModel.play(detailModel.currentGenre.value!!, false) + R.id.action_play -> playbackModel.playGenre( + detailModel.currentGenre.value!!, false + ) } true diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index 02a42ba77..a843020b0 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -25,7 +25,7 @@ import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.playback.PlaybackMode +import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.theme.applyColor import org.oxycblt.auxio.theme.applyDivider @@ -119,8 +119,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { } } - // FIXME: Change LibraryAdapter to a ListAdapter - // [If there's a way to preserve scroll position properly] binding.libraryRecycler.apply { adapter = libraryAdapter @@ -183,7 +181,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { // If the item is a song [That was selected through search], then update the playback // to that song instead of doing any navigation if (baseModel is Song) { - playbackModel.update(baseModel, PlaybackMode.ALL_SONGS) + playbackModel.playSong(baseModel, PlaybackMode.ALL_SONGS) return } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt index 7ab8ae7d2..256faf423 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt @@ -16,7 +16,9 @@ import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding import org.oxycblt.auxio.music.MusicStore class CompactPlaybackFragment : Fragment() { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels { + PlaybackViewModel.Factory(requireContext()) + } override fun onCreateView( inflater: LayoutInflater, @@ -50,7 +52,7 @@ class CompactPlaybackFragment : Fragment() { // --- VIEWMODEL SETUP --- - playbackModel.currentSong.observe(viewLifecycleOwner) { + playbackModel.song.observe(viewLifecycleOwner) { if (it != null) { Log.d(this::class.simpleName, "Updating song display to ${it.name}") @@ -78,7 +80,7 @@ class CompactPlaybackFragment : Fragment() { } } - playbackModel.formattedSeekBarProgress.observe(viewLifecycleOwner) { + playbackModel.positionAsProgress.observe(viewLifecycleOwner) { binding.playbackProgress.progress = it } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index c157dafac..bb42d9ad8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -51,7 +51,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { binding.lifecycleOwner = viewLifecycleOwner binding.playbackModel = playbackModel - binding.song = playbackModel.currentSong.value!! + binding.song = playbackModel.song.value!! binding.playbackToolbar.apply { setNavigationOnClickListener { @@ -73,14 +73,14 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { // --- VIEWMODEL SETUP -- - playbackModel.currentSong.observe(viewLifecycleOwner) { + playbackModel.song.observe(viewLifecycleOwner) { Log.d(this::class.simpleName, "Updating song display to ${it.name}.") binding.song = it binding.playbackSeekBar.max = it.seconds.toInt() } - playbackModel.currentIndex.observe(viewLifecycleOwner) { + playbackModel.index.observe(viewLifecycleOwner) { if (it > 0) { binding.playbackSkipPrev.enable(requireContext()) } else { @@ -129,11 +129,11 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { } // Updates for the current duration TextView/Seekbar - playbackModel.formattedCurrentDuration.observe(viewLifecycleOwner) { + playbackModel.formattedPosition.observe(viewLifecycleOwner) { binding.playbackDurationCurrent.text = it } - playbackModel.formattedSeekBarProgress.observe(viewLifecycleOwner) { + playbackModel.positionAsProgress.observe(viewLifecycleOwner) { binding.playbackSeekBar.progress = it } @@ -145,7 +145,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { // Seeking callbacks override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { if (fromUser) { - playbackModel.updateCurrentDurationWithProgress(progress) + playbackModel.updatePositionWithProgress(progress) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt index 61fba8d3b..eef5d726d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt @@ -10,8 +10,10 @@ import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.SimpleExoPlayer import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.toURI +import org.oxycblt.auxio.playback.state.PlaybackStateCallback +import org.oxycblt.auxio.playback.state.PlaybackStateManager -class PlaybackService : Service(), Player.EventListener { +class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { private val player: SimpleExoPlayer by lazy { val p = SimpleExoPlayer.Builder(applicationContext).build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -22,23 +24,32 @@ class PlaybackService : Service(), Player.EventListener { } private val mBinder = LocalBinder() + private val playbackManager = PlaybackStateManager.getInstance() override fun onBind(intent: Intent): IBinder { return mBinder } + override fun onCreate() { + super.onCreate() + + playbackManager.addCallback(this) + } + override fun onDestroy() { super.onDestroy() player.release() + playbackManager.removeCallback(this) } - fun playSong(song: Song) { - val item = MediaItem.fromUri(song.id.toURI()) - - player.setMediaItem(item) - player.prepare() - player.play() + override fun onSongUpdate(song: Song?) { + song?.let { + val item = MediaItem.fromUri(it.id.toURI()) + player.setMediaItem(item) + player.prepare() + player.play() + } } inner class LocalBinder : Binder() { 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 e25d0fb10..0d7b472b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -11,55 +11,60 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.google.android.exoplayer2.Player import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.toDuration -import kotlin.random.Random -import kotlin.random.Random.Default.nextLong +import org.oxycblt.auxio.playback.state.PlaybackMode +import org.oxycblt.auxio.playback.state.PlaybackStateCallback +import org.oxycblt.auxio.playback.state.PlaybackStateManager -// TODO: User managed queue -// TODO: Add the playback service itself -// TODO: Add loop control [From playback] -// TODO: Implement persistence through Bundles [I want to keep my shuffles, okay?] -// A ViewModel that acts as an intermediary between PlaybackService and the Playback Fragments. -class PlaybackViewModel(private val context: Context) : ViewModel(), Player.EventListener { - private val mCurrentSong = MutableLiveData() - val currentSong: LiveData get() = mCurrentSong +// The UI frontend for PlaybackStateManager. +class PlaybackViewModel(private val context: Context) : ViewModel(), PlaybackStateCallback { + // Playback + private val mSong = MutableLiveData() + val song: LiveData get() = mSong - private val mCurrentParent = MutableLiveData() - val currentParent: LiveData get() = mCurrentParent + private val mPosition = MutableLiveData(0L) + val position: LiveData get() = mPosition + // Queue private val mQueue = MutableLiveData(mutableListOf()) val queue: LiveData> get() = mQueue - private val mCurrentIndex = MutableLiveData(0) - val currentIndex: LiveData get() = mCurrentIndex - - private val mCurrentMode = MutableLiveData(PlaybackMode.ALL_SONGS) - val currentMode: LiveData get() = mCurrentMode - - private val mCurrentDuration = MutableLiveData(0L) + private val mIndex = MutableLiveData(0) + val index: LiveData get() = mIndex + // States private val mIsPlaying = MutableLiveData(false) val isPlaying: LiveData get() = mIsPlaying private val mIsShuffling = MutableLiveData(false) val isShuffling: LiveData get() = mIsShuffling - private val mShuffleSeed = MutableLiveData(-1L) - val shuffleSeed: LiveData get() = mShuffleSeed - + // Other private val mIsSeeking = MutableLiveData(false) val isSeeking: LiveData get() = mIsSeeking + val formattedPosition = Transformations.map(mPosition) { + it.toDuration() + } + + val positionAsProgress = Transformations.map(mPosition) { + if (mSong.value != null) it.toInt() else 0 + } + + val nextItemsInQueue = Transformations.map(mQueue) { + it.slice((mIndex.value!! + 1) until it.size) + } + private var mCanAnimate = false val canAnimate: Boolean get() = mCanAnimate + // Service setup + private val playbackManager = PlaybackStateManager.getInstance() + private lateinit var playbackService: PlaybackService private var playbackIntent: Intent @@ -79,342 +84,137 @@ class PlaybackViewModel(private val context: Context) : ViewModel(), Player.Even } } - // Formatted variants of the duration - val formattedCurrentDuration = Transformations.map(mCurrentDuration) { - it.toDuration() + init { + playbackManager.addCallback(this) } - val formattedSeekBarProgress = Transformations.map(mCurrentDuration) { - if (mCurrentSong.value != null) it.toInt() else 0 + // --- PLAYING FUNCTIONS --- + + fun playSong(song: Song, mode: PlaybackMode) { + playbackManager.playSong(song, mode) } - // Formatted queue that shows all the songs after the current playing song. - val formattedQueue = Transformations.map(mQueue) { - it.slice((mCurrentIndex.value!! + 1) until it.size) + fun shuffleAll() { + playbackManager.shuffleAll() } - // Update the current song while changing the queue mode. - fun update(song: Song, mode: PlaybackMode) { - // Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible - // to determine what genre a song has. - if (mode == PlaybackMode.IN_GENRE) { - Log.e(this::class.simpleName, "Auxio cant play songs with the mode of IN_GENRE.") - - return - } - - Log.d(this::class.simpleName, "Updating song to ${song.name} and mode to $mode") - - val musicStore = MusicStore.getInstance() - - mCurrentMode.value = mode - - updatePlayback(song) - - mQueue.value = when (mode) { - PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList() - PlaybackMode.IN_ARTIST -> song.album.artist.songs - PlaybackMode.IN_ALBUM -> song.album.songs - PlaybackMode.IN_GENRE -> error("what") - } - - mCurrentParent.value = when (mode) { - PlaybackMode.ALL_SONGS -> null - PlaybackMode.IN_ARTIST -> song.album.artist - PlaybackMode.IN_ALBUM -> song.album - PlaybackMode.IN_GENRE -> error("what") - } - - if (mIsShuffling.value!!) { - genShuffle(true) - } else { - resetShuffle() - } - - mCurrentIndex.value = mQueue.value!!.indexOf(song) - } - - // Play some parent music model, whether that being albums/artists/genres. - fun play(album: Album, isShuffled: Boolean) { - Log.d(this::class.simpleName, "Playing album ${album.name}") - + fun playAlbum(album: Album, shuffled: Boolean) { if (album.songs.isEmpty()) { - Log.e(this::class.simpleName, "Album is empty, not playing.") + Log.e(this::class.simpleName, "Album is empty, Not playing.") return } - val songs = orderSongsInAlbum(album) - - updatePlayback(songs[0]) - - mQueue.value = songs - mCurrentIndex.value = 0 - mCurrentParent.value = album - mIsShuffling.value = isShuffled - mCurrentMode.value = PlaybackMode.IN_ALBUM - - if (mIsShuffling.value!!) { - genShuffle(false) - } else { - resetShuffle() - } + playbackManager.playParentModel(album, shuffled) } - fun play(artist: Artist, isShuffled: Boolean) { - Log.d(this::class.simpleName, "Playing artist ${artist.name}") - + fun playArtist(artist: Artist, shuffled: Boolean) { if (artist.songs.isEmpty()) { - Log.e(this::class.simpleName, "Artist is empty, not playing.") + Log.e(this::class.simpleName, "Artist is empty, Not playing.") return } - val songs = orderSongsInArtist(artist) - - updatePlayback(songs[0]) - - mQueue.value = songs - mCurrentIndex.value = 0 - mCurrentParent.value = artist - mIsShuffling.value = isShuffled - mCurrentMode.value = PlaybackMode.IN_ARTIST - - if (mIsShuffling.value!!) { - genShuffle(false) - } else { - resetShuffle() - } + playbackManager.playParentModel(artist, shuffled) } - fun play(genre: Genre, isShuffled: Boolean) { - Log.d(this::class.simpleName, "Playing genre ${genre.name}") - + fun playGenre(genre: Genre, shuffled: Boolean) { if (genre.songs.isEmpty()) { - Log.e(this::class.simpleName, "Genre is empty, not playing.") + Log.e(this::class.simpleName, "Genre is empty, Not playing.") return } - val songs = orderSongsInGenre(genre) - - updatePlayback(songs[0]) - - mQueue.value = songs - mCurrentIndex.value = 0 - mCurrentParent.value = genre - mIsShuffling.value = isShuffled - mCurrentMode.value = PlaybackMode.IN_GENRE - - if (mIsShuffling.value!!) { - genShuffle(false) - } else { - resetShuffle() - } + playbackManager.playParentModel(genre, shuffled) } - // Update the current duration using a SeekBar progress - fun updateCurrentDurationWithProgress(progress: Int) { - mCurrentDuration.value = progress.toLong() + // --- POSITION FUNCTIONS --- + + fun updatePositionWithProgress(progress: Int) { + playbackManager.setPosition(progress.toLong()) } - // Invert, not directly set the playing/shuffling status - // Used by the toggle buttons in playback fragment. + // --- QUEUE FUNCTIONS --- + + fun skipNext() { + playbackManager.skipNext() + } + + fun skipPrev() { + playbackManager.skipPrev() + } + + fun removeQueueItem(adapterIndex: Int) { + // Translate the adapter indices into the correct queue indices + val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size + + val index = adapterIndex + delta + + playbackManager.removeQueueItem(index) + } + + fun moveQueueItems(adapterFrom: Int, adapterTo: Int) { + // Translate the adapter indices into the correct queue indices + val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size + + val from = adapterFrom + delta + val to = adapterTo + delta + + playbackManager.moveQueueItems(from, to) + } + + // --- STATUS FUNCTIONS --- + fun invertPlayingStatus() { mCanAnimate = true - mIsPlaying.value = !mIsPlaying.value!! + playbackManager.setPlayingStatus(!playbackManager.isPlaying) } fun invertShuffleStatus() { - mIsShuffling.value = !mIsShuffling.value!! - - if (mIsShuffling.value!!) { - genShuffle(true) - } else { - resetShuffle() - } + playbackManager.setShuffleStatus(!playbackManager.isShuffling) } - // Shuffle all the songs. - fun shuffleAll() { - val musicStore = MusicStore.getInstance() - - mIsShuffling.value = true - mQueue.value = musicStore.songs.toMutableList() - mCurrentMode.value = PlaybackMode.ALL_SONGS - mCurrentIndex.value = 0 - - genShuffle(false) - updatePlayback(mQueue.value!![0]) - } - - // Set the seeking status - fun setSeekingStatus(status: Boolean) { - mIsSeeking.value = status - } - - // Skip to next song - fun skipNext() { - if (mCurrentIndex.value!! < mQueue.value!!.size) { - mCurrentIndex.value = mCurrentIndex.value!!.inc() - } - - updatePlayback(mQueue.value!![mCurrentIndex.value!!]) - - forceQueueUpdate() - } - - // Skip to last song - fun skipPrev() { - if (mCurrentIndex.value!! > 0) { - mCurrentIndex.value = mCurrentIndex.value!!.dec() - } - - updatePlayback(mQueue.value!![mCurrentIndex.value!!]) - - forceQueueUpdate() - } + // --- OTHER FUNCTIONS --- fun resetAnimStatus() { mCanAnimate = false } - // Move two queue items. Note that this function does not force-update the queue, - // as calling updateData with a drag would cause bugs. - fun moveQueueItems(adapterFrom: Int, adapterTo: Int) { - // Translate the adapter indices into the correct queue indices - val delta = mQueue.value!!.size - formattedQueue.value!!.size - - val from = adapterFrom + delta - val to = adapterTo + delta - - try { - val currentItem = mQueue.value!![from] - - mQueue.value!!.removeAt(from) - mQueue.value!!.add(to, currentItem) - } catch (exception: IndexOutOfBoundsException) { - Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item") - - return - } - - forceQueueUpdate() + fun setSeekingStatus(value: Boolean) { + mIsSeeking.value = value } - // Remove a queue item. Note that this function does not force-update the queue, - // as calling updateData with a drag would cause bugs. - fun removeQueueItem(adapterIndex: Int) { - // Translate the adapter index into the correct queue index - val delta = mQueue.value!!.size - formattedQueue.value!!.size - val properIndex = adapterIndex + delta - - Log.d(this::class.simpleName, "Removing item ${mQueue.value!![properIndex].name}.") - - if (properIndex > mQueue.value!!.size || properIndex < 0) { - Log.e(this::class.simpleName, "Index is out of bounds, did not remove queue item.") - - return - } - - mQueue.value!!.removeAt(properIndex) - - forceQueueUpdate() - } - - // Force the observers of the queue to actually update after making changes. - private fun forceQueueUpdate() { - mQueue.value = mQueue.value - } - - // Generic function for updating the playback with a new song. - // Use this instead of manually updating the values each time. - private fun updatePlayback(song: Song) { - mCurrentSong.value = song - mCurrentDuration.value = 0 - - if (!mIsPlaying.value!!) { - mIsPlaying.value = true - } - - playbackService.playSong(song) - } - - // Generate a new shuffled queue. - private fun genShuffle(keepSong: Boolean) { - // Take a random seed and then shuffle the current queue based off of that. - // This seed will be saved in a bundle if the app closes, so that the shuffle mode - // can be restored when its started again. - val newSeed = Random.Default.nextLong() - - Log.d(this::class.simpleName, "Shuffling queue with a seed of $newSeed.") - - mShuffleSeed.value = newSeed - - mQueue.value!!.shuffle(Random(newSeed)) - mCurrentIndex.value = 0 - - // If specified, make the current song the first member of the queue. - if (keepSong) { - mQueue.value!!.remove(mCurrentSong.value) - mQueue.value!!.add(0, mCurrentSong.value!!) - } else { - // Otherwise, just start from the zeroth position in the queue. - mCurrentSong.value = mQueue.value!![0] - } - - // Force the observers to actually update. - mQueue.value = mQueue.value - } - - // Stop the queue and attempt to restore to the previous state - private fun resetShuffle() { - mShuffleSeed.value = -1 - - mQueue.value = when (mCurrentMode.value!!) { - PlaybackMode.IN_ARTIST -> orderSongsInArtist(mCurrentParent.value as Artist) - PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mCurrentParent.value as Album) - PlaybackMode.IN_GENRE -> orderSongsInGenre(mCurrentParent.value as Genre) - PlaybackMode.ALL_SONGS -> MusicStore.getInstance().songs.toMutableList() - } - - mCurrentIndex.value = mQueue.value!!.indexOf(mCurrentSong.value) - } - - // Basic sorting functions when things are played in order - private fun orderSongsInAlbum(album: Album): MutableList { - return album.songs.sortedBy { it.track }.toMutableList() - } - - private fun orderSongsInArtist(artist: Artist): MutableList { - val final = mutableListOf() - - artist.albums.sortedByDescending { it.year }.forEach { album -> - final.addAll(album.songs.sortedBy { it.track }) - } - - return final - } - - private fun orderSongsInGenre(genre: Genre): MutableList { - val final = mutableListOf() - - genre.artists.sortedWith( - compareBy(String.CASE_INSENSITIVE_ORDER) { it.name } - ).forEach { artist -> - artist.albums.sortedByDescending { it.year }.forEach { album -> - final.addAll(album.songs.sortedBy { it.track }) - } - } - - return final - } + // --- OVERRIDES --- override fun onCleared() { - super.onCleared() + playbackManager.removeCallback(this) + } - context.unbindService(connection) + override fun onSongUpdate(song: Song?) { + song?.let { + mSong.value = it + } + } + + override fun onPositionUpdate(position: Long) { + mPosition.value = position + } + + override fun onQueueUpdate(queue: MutableList) { + mQueue.value = queue + } + + override fun onIndexUpdate(index: Int) { + mIndex.value = index + } + + override fun onPlayingUpdate(isPlaying: Boolean) { + mIsPlaying.value = isPlaying + } + + override fun onShuffleUpdate(isShuffling: Boolean) { + mIsShuffling.value = isShuffling } class Factory(private val context: Context) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index ccafbca0f..4ebdd988d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -16,7 +16,9 @@ import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.toColor class QueueFragment : BottomSheetDialogFragment() { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels { + PlaybackViewModel.Factory(requireActivity().application) + } override fun getTheme(): Int = R.style.Theme_BottomSheetFix @@ -46,7 +48,7 @@ class QueueFragment : BottomSheetDialogFragment() { // --- VIEWMODEL SETUP --- - playbackModel.formattedQueue.observe(viewLifecycleOwner) { + playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { // If the first item is being moved, then scroll to the top position on completion // to prevent ListAdapter from scrolling uncontrollably. if (queueAdapter.currentList.isNotEmpty() && it[0].id != queueAdapter.currentList[0].id) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackMode.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt similarity index 86% rename from app/src/main/java/org/oxycblt/auxio/playback/PlaybackMode.kt rename to app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt index 43ba15d34..59adc65df 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt @@ -1,4 +1,4 @@ -package org.oxycblt.auxio.playback +package org.oxycblt.auxio.playback.state // Enum for instruction how the queue should function. // ALL SONGS -> Play from all songs diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt new file mode 100644 index 000000000..94ed7eebb --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt @@ -0,0 +1,12 @@ +package org.oxycblt.auxio.playback.state + +import org.oxycblt.auxio.music.Song + +interface PlaybackStateCallback { + fun onSongUpdate(song: Song?) {} + fun onPositionUpdate(position: Long) {} + fun onQueueUpdate(queue: MutableList) {} + fun onPlayingUpdate(isPlaying: Boolean) {} + fun onShuffleUpdate(isShuffling: Boolean) {} + fun onIndexUpdate(index: Int) {} +} 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 new file mode 100644 index 000000000..0da5b9dde --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -0,0 +1,344 @@ +package org.oxycblt.auxio.playback.state + +import android.util.Log +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.BaseModel +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Header +import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.music.Song +import kotlin.random.Random + +// The manager of the current playback state [Current Song, Queue, Shuffling] +// Never use this for ANYTHING UI related, that's what PlaybackViewModel is for. +class PlaybackStateManager { + // Playback + private var mSong: Song? = null + set(value) { + field = value + callbacks.forEach { it.onSongUpdate(value) } + } + private var mPosition: Long = 0 + set(value) { + field = value + callbacks.forEach { it.onPositionUpdate(value) } + } + private var mParent: BaseModel? = null + + // Queue + private var mQueue = mutableListOf() + set(value) { + field = value + callbacks.forEach { it.onQueueUpdate(value) } + } + private var mIndex = 0 + set(value) { + field = value + callbacks.forEach { it.onIndexUpdate(value) } + } + private var mMode = PlaybackMode.ALL_SONGS + + private var mIsPlaying = false + set(value) { + field = value + callbacks.forEach { it.onPlayingUpdate(value) } + } + + private var mIsShuffling = false + set(value) { + field = value + callbacks.forEach { it.onShuffleUpdate(value) } + } + private var mShuffleSeed = -1L + + val isPlaying: Boolean get() = mIsPlaying + val isShuffling: Boolean get() = mIsShuffling + + // --- CALLBACKS --- + + private val callbacks = mutableListOf() + + fun addCallback(callback: PlaybackStateCallback) { + callbacks.add(callback) + } + + fun removeCallback(callback: PlaybackStateCallback) { + callbacks.remove(callback) + } + + // --- PLAYING FUNCTIONS --- + + fun playSong(song: Song, mode: PlaybackMode) { + // Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible + // to determine what genre a song has. + if (mode == PlaybackMode.IN_GENRE) { + Log.e(this::class.simpleName, "Auxio cant play songs with the mode of IN_GENRE.") + + return + } + + Log.d(this::class.simpleName, "Updating song to ${song.name} and mode to $mode") + + val musicStore = MusicStore.getInstance() + + mMode = mode + + updatePlayback(song) + + mQueue = when (mode) { + PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList() + PlaybackMode.IN_ARTIST -> song.album.artist.songs + PlaybackMode.IN_ALBUM -> song.album.songs + PlaybackMode.IN_GENRE -> error("what") + } + + mParent = when (mode) { + PlaybackMode.ALL_SONGS -> null + PlaybackMode.IN_ARTIST -> song.album.artist + PlaybackMode.IN_ALBUM -> song.album + PlaybackMode.IN_GENRE -> error("what") + } + + if (mIsShuffling) { + genShuffle(true) + } else { + resetShuffle() + } + + mIndex = mQueue.indexOf(song) + } + + fun playParentModel(baseModel: BaseModel, shuffled: Boolean) { + if (baseModel is Song || baseModel is Header) { + Log.e( + this::class.simpleName, + "playParentModel does not support ${baseModel::class.simpleName}." + ) + + return + } + + Log.d(this::class.simpleName, "Playing ${baseModel.name}") + + when (baseModel) { + is Album -> { + mQueue = orderSongsInAlbum(baseModel) + mMode = PlaybackMode.IN_ALBUM + } + is Artist -> { + mQueue = orderSongsInArtist(baseModel) + mMode = PlaybackMode.IN_ARTIST + } + is Genre -> { + mQueue = orderSongsInGenre(baseModel) + mMode = PlaybackMode.IN_GENRE + } + + else -> error("what") + } + + updatePlayback(mQueue[0]) + + mIndex = 0 + mParent = baseModel + mIsShuffling = shuffled + + if (mIsShuffling) { + genShuffle(false) + } else { + resetShuffle() + } + } + + private fun updatePlayback(song: Song) { + mSong = song + mPosition = 0 + + if (!mIsPlaying) { + mIsPlaying = true + } + } + + fun setPosition(position: Long) { + mPosition = position + } + + // --- QUEUE FUNCTIONS --- + + fun skipNext() { + if (mIndex < mQueue.size) { + mIndex = mIndex.inc() + } + + updatePlayback(mQueue[mIndex]) + + forceQueueUpdate() + } + + fun skipPrev() { + if (mIndex > 0) { + mIndex = mIndex.dec() + } + + updatePlayback(mQueue[mIndex]) + + forceQueueUpdate() + } + + fun removeQueueItem(index: Int) { + Log.d(this::class.simpleName, "Removing item ${mQueue[index].name}.") + + if (index > mQueue.size || index < 0) { + Log.e(this::class.simpleName, "Index is out of bounds, did not remove queue item.") + + return + } + + mQueue.removeAt(index) + + forceQueueUpdate() + } + + fun moveQueueItems(from: Int, to: Int) { + try { + val currentItem = mQueue[from] + + mQueue.removeAt(from) + mQueue.add(to, currentItem) + } catch (exception: IndexOutOfBoundsException) { + Log.e(this::class.simpleName, "Indices were out of bounds, did not move queue item") + + return + } + + forceQueueUpdate() + } + + private fun forceQueueUpdate() { + mQueue = mQueue + } + + // --- SHUFFLE FUNCTIONS --- + + fun shuffleAll() { + val musicStore = MusicStore.getInstance() + + mIsShuffling = true + mQueue = musicStore.songs.toMutableList() + mMode = PlaybackMode.ALL_SONGS + mIndex = 0 + + genShuffle(false) + updatePlayback(mQueue[0]) + } + + // Generate a new shuffled queue. + private fun genShuffle(keepSong: Boolean) { + // Take a random seed and then shuffle the current queue based off of that. + // This seed will be saved in a bundle if the app closes, so that the shuffle mode + // can be restored when its started again. + val newSeed = Random.Default.nextLong() + + Log.d(this::class.simpleName, "Shuffling queue with a seed of $newSeed.") + + mShuffleSeed = newSeed + + mQueue.shuffle(Random(newSeed)) + mIndex = 0 + + // If specified, make the current song the first member of the queue. + if (keepSong) { + mSong?.let { + mQueue.remove(it) + mQueue.add(0, it) + } + } else { + // Otherwise, just start from the zeroth position in the queue. + mSong = mQueue[0] + } + + forceQueueUpdate() + } + + // Stop the queue and attempt to restore to the previous state + private fun resetShuffle() { + mShuffleSeed = -1 + + mQueue = when (mMode) { + PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist) + PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album) + PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre) + PlaybackMode.ALL_SONGS -> MusicStore.getInstance().songs.toMutableList() + } + + mIndex = mQueue.indexOf(mSong) + } + + // --- STATE FUNCTIONS --- + + fun setPlayingStatus(value: Boolean) { + if (mIsPlaying != value) { + mIsPlaying = value + } + } + + fun setShuffleStatus(value: Boolean) { + mIsShuffling = value + + if (mIsShuffling) { + genShuffle(true) + } else { + resetShuffle() + } + } + + // --- ORDERING FUNCTIONS --- + + private fun orderSongsInAlbum(album: Album): MutableList { + return album.songs.sortedBy { it.track }.toMutableList() + } + + private fun orderSongsInArtist(artist: Artist): MutableList { + val final = mutableListOf() + + artist.albums.sortedByDescending { it.year }.forEach { album -> + final.addAll(album.songs.sortedBy { it.track }) + } + + return final + } + + private fun orderSongsInGenre(genre: Genre): MutableList { + val final = mutableListOf() + + genre.artists.sortedWith( + compareBy(String.CASE_INSENSITIVE_ORDER) { it.name } + ).forEach { artist -> + artist.albums.sortedByDescending { it.year }.forEach { album -> + final.addAll(album.songs.sortedBy { it.track }) + } + } + + return final + } + + companion object { + @Volatile + private var INSTANCE: PlaybackStateManager? = null + + fun getInstance(): PlaybackStateManager { + val currentInstance = INSTANCE + + if (currentInstance != null) { + return currentInstance + } + + synchronized(this) { + val newInstance = PlaybackStateManager() + INSTANCE = newInstance + return newInstance + } + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt index 9c8b85991..608b593de 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -10,7 +10,7 @@ import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentSongsBinding import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.playback.PlaybackMode +import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.theme.applyDivider @@ -42,7 +42,7 @@ class SongsFragment : Fragment() { binding.songRecycler.apply { adapter = SongAdapter(musicStore.songs) { - playbackModel.update(it, PlaybackMode.ALL_SONGS) + playbackModel.playSong(it, PlaybackMode.ALL_SONGS) } applyDivider() setHasFixedSize(true)