Add ability to clear user queue

Add an option to clear the user queue in QueueFragment.
This commit is contained in:
OxygenCobalt 2020-11-22 15:25:23 -07:00
parent 49897c53b4
commit 1d50d24c4f
11 changed files with 82 additions and 69 deletions

View file

@ -8,7 +8,10 @@ import android.database.sqlite.SQLiteOpenHelper
import android.util.Log import android.util.Log
/** /**
* A bootstrapped SQLite database for managing the persistent playback state and queue. * A SQLite database for managing the persistent playback state and queue.
* Yes, I know androidx has Room which supposedly makes database creation easier, but it also
* has a crippling bug where it will endlessly allocate rows even if you clear the entire db, so...
* @author OxygenCobalt
*/ */
class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {
@ -64,6 +67,9 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
// --- INTERFACE FUNCTIONS --- // --- INTERFACE FUNCTIONS ---
/**
* Clear the previously written [PlaybackState] and write a new one.
*/
fun writeState(state: PlaybackState) { fun writeState(state: PlaybackState) {
val database = writableDatabase val database = writableDatabase
database.beginTransaction() database.beginTransaction()
@ -102,6 +108,11 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
} }
} }
/**
* Read the stored [PlaybackState] from the database, if there is one.
* @return The stored [PlaybackState], null if there isnt one,.
* @author OxygenCobalt
*/
fun readState(): PlaybackState? { fun readState(): PlaybackState? {
val database = writableDatabase val database = writableDatabase
@ -112,6 +123,7 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
stateCursor = database.query(TABLE_NAME_STATE, null, null, null, null, null, null) stateCursor = database.query(TABLE_NAME_STATE, null, null, null, null, null, null)
stateCursor?.use { cursor -> stateCursor?.use { cursor ->
// Don't bother if the cursor [and therefore database] has nothing in it.
if (cursor.count == 0) return@use if (cursor.count == 0) return@use
val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_ID) val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_ID)
@ -127,6 +139,7 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
val inUserQueueIndex = val inUserQueueIndex =
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IN_USER_QUEUE) cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IN_USER_QUEUE)
// If there is something in it, get the first item from it, ignoring anything else.
cursor.moveToFirst() cursor.moveToFirst()
state = PlaybackState( state = PlaybackState(
@ -147,6 +160,11 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
} }
} }
/**
* Write a list of [QueueItem]s to the database, clearing the previous queue present.
* @param queue The list of [QueueItem]s to be written.
* @author OxygenCobalt
*/
fun writeQueue(queue: List<QueueItem>) { fun writeQueue(queue: List<QueueItem>) {
val database = readableDatabase val database = readableDatabase
database.beginTransaction() database.beginTransaction()
@ -196,6 +214,11 @@ class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAM
} }
} }
/**
* Read the database for any [QueueItem]s.
* @return A list of any stored [QueueItem]s.
* @author OxygenCobalt
*/
fun readQueue(): List<QueueItem> { fun readQueue(): List<QueueItem> {
val database = readableDatabase val database = readableDatabase

View file

@ -120,6 +120,5 @@ data class Genre(
*/ */
data class Header( data class Header(
override val id: Long = -1, override val id: Long = -1,
override var name: String = "", override var name: String = ""
val isAction: Boolean = false
) : BaseModel() ) : BaseModel()

View file

@ -159,7 +159,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
} }
// Remove a queue OR user queue item, given a QueueAdapter index. // Remove a queue OR user queue item, given a QueueAdapter index.
fun removeQueueItem(adapterIndex: Int, queueAdapter: QueueAdapter) { fun removeQueueAdapterItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
var index = adapterIndex.dec() var index = adapterIndex.dec()
// If the item is in the user queue, then remove it from there after accounting for the header. // If the item is in the user queue, then remove it from there after accounting for the header.
@ -182,7 +182,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
} }
// Move queue OR user queue items, given QueueAdapter indices. // Move queue OR user queue items, given QueueAdapter indices.
fun moveQueueItems(adapterFrom: Int, adapterTo: Int, queueAdapter: QueueAdapter): Boolean { fun moveQueueAdapterItems(adapterFrom: Int, adapterTo: Int, queueAdapter: QueueAdapter): Boolean {
var from = adapterFrom.dec() var from = adapterFrom.dec()
var to = adapterTo.dec() var to = adapterTo.dec()
@ -230,6 +230,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.addToUserQueue(songs) playbackManager.addToUserQueue(songs)
} }
fun clearUserQueue() {
playbackManager.clearUserQueue()
}
// --- STATUS FUNCTIONS --- // --- STATUS FUNCTIONS ---
// Flip the playing status. // Flip the playing status.

View file

@ -98,6 +98,14 @@ class QueueAdapter(
} }
} }
fun clearUserQueue() {
val nextQueueHeaderIndex = data.indexOfLast { it is Header }
val slice = data.slice(0 until nextQueueHeaderIndex)
data.removeAll(slice)
notifyItemRangeRemoved(0, slice.size)
}
// Generic ViewHolder for a queue item // Generic ViewHolder for a queue item
inner class QueueSongViewHolder( inner class QueueSongViewHolder(
private val binding: ItemQueueSongBinding, private val binding: ItemQueueSongBinding,

View file

@ -53,7 +53,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
): Boolean { ): Boolean {
return playbackModel.moveQueueItems( return playbackModel.moveQueueAdapterItems(
viewHolder.adapterPosition, viewHolder.adapterPosition,
target.adapterPosition, target.adapterPosition,
queueAdapter queueAdapter
@ -61,7 +61,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
playbackModel.removeQueueItem(viewHolder.adapterPosition, queueAdapter) playbackModel.removeQueueAdapterItem(viewHolder.adapterPosition, queueAdapter)
} }
fun addQueueAdapter(adapter: QueueAdapter) { fun addQueueAdapter(adapter: QueueAdapter) {

View file

@ -37,10 +37,22 @@ class QueueFragment : Fragment() {
val queueAdapter = QueueAdapter(helper) val queueAdapter = QueueAdapter(helper)
callback.addQueueAdapter(queueAdapter) callback.addQueueAdapter(queueAdapter)
val queueClearItem = binding.queueToolbar.menu.findItem(R.id.action_clear_user_queue)
// --- UI SETUP --- // --- UI SETUP ---
binding.queueToolbar.setNavigationOnClickListener { binding.queueToolbar.apply {
findNavController().navigateUp() setNavigationOnClickListener {
findNavController().navigateUp()
}
setOnMenuItemClickListener {
if (it.itemId == R.id.action_clear_user_queue) {
queueAdapter.clearUserQueue()
playbackModel.clearUserQueue()
true
} else false
}
} }
binding.queueRecycler.apply { binding.queueRecycler.apply {
@ -53,8 +65,14 @@ class QueueFragment : Fragment() {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
playbackModel.userQueue.observe(viewLifecycleOwner) { playbackModel.userQueue.observe(viewLifecycleOwner) {
if (it.isEmpty() && playbackModel.nextItemsInQueue.value!!.isEmpty()) { if (it.isEmpty()) {
findNavController().navigateUp() queueClearItem.isEnabled = false
if (playbackModel.nextItemsInQueue.value!!.isEmpty()) {
findNavController().navigateUp()
return@observe
}
} }
queueAdapter.submitList(createQueueData()) queueAdapter.submitList(createQueueData())
@ -76,10 +94,7 @@ class QueueFragment : Fragment() {
if (playbackModel.userQueue.value!!.isNotEmpty()) { if (playbackModel.userQueue.value!!.isNotEmpty()) {
queue.add( queue.add(
Header( Header(name = getString(R.string.label_next_user_queue))
name = getString(R.string.label_next_user_queue),
isAction = true
)
) )
queue.addAll(playbackModel.userQueue.value!!) queue.addAll(playbackModel.userQueue.value!!)
} }
@ -93,8 +108,7 @@ class QueueFragment : Fragment() {
getString(R.string.label_all_songs) getString(R.string.label_all_songs)
else else
playbackModel.parent.value!!.name playbackModel.parent.value!!.name
), )
isAction = false
) )
) )
queue.addAll(playbackModel.nextItemsInQueue.value!!) queue.addAll(playbackModel.nextItemsInQueue.value!!)

View file

@ -337,6 +337,12 @@ class PlaybackStateManager private constructor() {
forceUserQueueUpdate() forceUserQueueUpdate()
} }
fun clearUserQueue() {
mUserQueue.clear()
forceUserQueueUpdate()
}
// Force any callbacks to update when the queue is changed. // Force any callbacks to update when the queue is changed.
private fun forceQueueUpdate() { private fun forceQueueUpdate() {
mQueue = mQueue mQueue = mQueue

View file

@ -20,6 +20,8 @@
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:popupTheme="@style/Widget.CustomPopup"
app:menu="@menu/menu_queue"
app:navigationIcon="@drawable/ic_down" app:navigationIcon="@drawable/ic_down"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View file

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.HeaderViewHolder">
<data>
<variable
name="header"
type="org.oxycblt.auxio.music.Header" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/header_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_semibold"
android:paddingStart="@dimen/padding_medium"
android:paddingTop="@dimen/padding_small"
android:paddingEnd="@dimen/padding_small"
android:paddingBottom="@dimen/padding_small"
android:text="@{header.name}"
android:textColor="?android:attr/textColorPrimary"
android:textSize="19sp"
app:layout_constraintEnd_toStartOf="@+id/header_action_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Songs" />
<ImageButton
android:id="@+id/header_action_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
android:src="@drawable/ic_clear"
tools:visibility="visible"
android:background="@drawable/ui_header_dividers"
app:layout_constraintBottom_toBottomOf="@+id/header_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="@+id/header_text"
tools:ignore="contentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_clear_user_queue"
android:title="@string/label_clear_user_queue"
app:showAsAction="never" />
</menu>

View file

@ -25,6 +25,7 @@
<string name="label_queue_add">Add to queue</string> <string name="label_queue_add">Add to queue</string>
<string name="label_queue_added">Added to queue</string> <string name="label_queue_added">Added to queue</string>
<string name="label_next_user_queue">Next in Queue</string> <string name="label_next_user_queue">Next in Queue</string>
<string name="label_clear_user_queue">Clear queue</string>
<string name="label_channel">Music Playback</string> <string name="label_channel">Music Playback</string>
<string name="label_service_playback">The music playback service for Auxio.</string> <string name="label_service_playback">The music playback service for Auxio.</string>