Fix bugs with Queue

- Fix a bug where the queue would scroll up when the player moved to another song.
- Fix bug where queue would show even with no songs.
- Fix a crash from removing the last song of a queue.
This commit is contained in:
OxygenCobalt 2020-11-06 20:06:23 -07:00
parent 1cab11ba9c
commit c664d22a43
8 changed files with 70 additions and 27 deletions

View file

@ -145,7 +145,8 @@ private fun newAction(action: String, context: Context): NotificationCompat.Acti
NotificationUtils.ACTION_SKIP_NEXT -> R.drawable.ic_skip_next NotificationUtils.ACTION_SKIP_NEXT -> R.drawable.ic_skip_next
NotificationUtils.ACTION_EXIT -> R.drawable.ic_exit NotificationUtils.ACTION_EXIT -> R.drawable.ic_exit
else -> R.drawable.ic_play
else -> R.drawable.ic_error
} }
return NotificationCompat.Action.Builder( return NotificationCompat.Action.Builder(

View file

@ -5,6 +5,7 @@ import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.SeekBar import android.widget.SeekBar
@ -46,6 +47,17 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
requireContext(), R.drawable.ic_play_to_pause requireContext(), R.drawable.ic_play_to_pause
) as AnimatedVectorDrawable ) as AnimatedVectorDrawable
// Can't set the tint of a MenuItem below Android 8, so use icons instead.
val iconQueueActive = ContextCompat.getDrawable(
requireContext(), R.drawable.ic_queue
)
val iconQueueInactive = ContextCompat.getDrawable(
requireContext(), R.drawable.ic_queue_inactive
)
val queueMenuItem: MenuItem
// --- UI SETUP --- // --- UI SETUP ---
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
@ -64,6 +76,8 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
true true
} }
queueMenuItem = menu.findItem(R.id.action_queue)
} }
// Make marquee scroll work // Make marquee scroll work
@ -163,6 +177,17 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
} }
} }
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
// Disable the option to open the queue if there's nothing in it.
if (it.isEmpty()) {
queueMenuItem.isEnabled = false
queueMenuItem.icon = iconQueueInactive
} else {
queueMenuItem.isEnabled = true
queueMenuItem.icon = iconQueueActive
}
}
Log.d(this::class.simpleName, "Fragment Created.") Log.d(this::class.simpleName, "Fragment Created.")
return binding.root return binding.root

View file

@ -39,12 +39,11 @@ import org.oxycblt.auxio.music.coil.getBitmap
import org.oxycblt.auxio.music.toURI import org.oxycblt.auxio.music.toURI
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.playback.state.PlaybackStateCallback
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
// A Service that manages the single ExoPlayer instance and manages the system-side // A Service that manages the single ExoPlayer instance and manages the system-side
// aspects of playback. // aspects of playback.
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Callback {
private val player: SimpleExoPlayer by lazy { private val player: SimpleExoPlayer by lazy {
SimpleExoPlayer.Builder(applicationContext).build() SimpleExoPlayer.Builder(applicationContext).build()
} }

View file

@ -12,13 +12,12 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.playback.state.PlaybackStateCallback
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
// A ViewModel that acts as an intermediary between the UI and PlaybackStateManager // A ViewModel that acts as an intermediary between the UI and PlaybackStateManager
// TODO: Implement User Queue // TODO: Implement User Queue
// TODO: Implement Persistence through a Database // TODO: Implement Persistence through a Database
class PlaybackViewModel : ViewModel(), PlaybackStateCallback { class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// Playback // Playback
private val mSong = MutableLiveData<Song?>() private val mSong = MutableLiveData<Song?>()
val song: LiveData<Song?> get() = mSong val song: LiveData<Song?> get() = mSong
@ -59,7 +58,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
it.slice((mIndex.value!! + 1) until it.size) it.slice((mIndex.value!! + 1) until it.size)
} }
// Service setup
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
init { init {

View file

@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
@ -47,11 +48,24 @@ class QueueFragment : BottomSheetDialogFragment() {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
if (it.isEmpty()) {
dismiss()
return@observe
}
// If the first item is being moved, then scroll to the top position on completion // If the first item is being moved, then scroll to the top position on completion
// to prevent ListAdapter from scrolling uncontrollably. // to prevent ListAdapter from scrolling uncontrollably.
if (queueAdapter.currentList.isNotEmpty() && it[0].id != queueAdapter.currentList[0].id) { if (queueAdapter.currentList.isNotEmpty() && it[0].id != queueAdapter.currentList[0].id) {
queueAdapter.submitList(it.toMutableList()) { queueAdapter.submitList(it.toMutableList()) {
binding.queueRecycler.scrollToPosition(0) // Make sure that the RecyclerView doesn't scroll to the top if the first item
// changed, but is not visible.
val firstItem = (binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstVisibleItemPosition()
if (firstItem == -1 || firstItem == 0) {
binding.queueRecycler.scrollToPosition(0)
}
} }
} else { } else {
queueAdapter.submitList(it.toMutableList()) queueAdapter.submitList(it.toMutableList())

View file

@ -1,17 +0,0 @@
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<Song>) {}
fun onModeUpdate(mode: PlaybackMode) {}
fun onIndexUpdate(index: Int) {}
fun onPlayingUpdate(isPlaying: Boolean) {}
fun onShuffleUpdate(isShuffling: Boolean) {}
fun onLoopUpdate(mode: LoopMode) {}
// Service callbacks
fun onSeekConfirm(position: Long) {}
}

View file

@ -77,13 +77,13 @@ class PlaybackStateManager private constructor() {
// --- CALLBACKS --- // --- CALLBACKS ---
private val callbacks = mutableListOf<PlaybackStateCallback>() private val callbacks = mutableListOf<Callback>()
fun addCallback(callback: PlaybackStateCallback) { fun addCallback(callback: Callback) {
callbacks.add(callback) callbacks.add(callback)
} }
fun removeCallback(callback: PlaybackStateCallback) { fun removeCallback(callback: Callback) {
callbacks.remove(callback) callbacks.remove(callback)
} }
@ -397,4 +397,16 @@ class PlaybackStateManager private constructor() {
} }
} }
} }
interface Callback {
fun onSongUpdate(song: Song?) {}
fun onPositionUpdate(position: Long) {}
fun onQueueUpdate(queue: MutableList<Song>) {}
fun onModeUpdate(mode: PlaybackMode) {}
fun onIndexUpdate(index: Int) {}
fun onPlayingUpdate(isPlaying: Boolean) {}
fun onShuffleUpdate(isShuffling: Boolean) {}
fun onLoopUpdate(mode: LoopMode) {}
fun onSeekConfirm(position: Long) {}
}
} }

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/inactive_color">
<path
android:fillColor="@android:color/white"
android:pathData="M15,6L3,6v2h12L15,6zM15,10L3,10v2h12v-2zM3,16h8v-2L3,14v2zM17,6v8.18c-0.31,-0.11 -0.65,-0.18 -1,-0.18 -1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3L19,8h3L22,6h-5z" />
</vector>