Make QueueAdapter Adapter instead of ListAdapter

Fix a stupid amount of bugs by changing QueueAdapter to a normal adapter with a differ and addition/movement functions instead of a ListAdapter.
This commit is contained in:
OxygenCobalt 2020-11-13 14:56:06 -07:00
parent 2aa630948b
commit 2ebee41ed0
15 changed files with 85 additions and 61 deletions

View file

@ -7,7 +7,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.music.processing
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Albums
@ -20,6 +21,7 @@ enum class MusicLoaderResponse {
// Class that loads music from the FileSystem.
// TODO: Add custom artist images from the filesystem
// TODO: Move genre loading of songs [Loads would take longer though]
class MusicLoader(
private val resolver: ContentResolver,
@ -163,9 +165,9 @@ class MusicLoader(
it.genres.add(genre)
}
}
cursor.close()
}
artistGenreCursor?.close()
}
Log.d(
@ -174,6 +176,7 @@ class MusicLoader(
)
}
@SuppressLint("InlinedApi")
private fun loadAlbums() {
Log.d(this::class.simpleName, "Starting album search...")
@ -225,6 +228,7 @@ class MusicLoader(
)
}
@SuppressLint("InlinedApi")
private fun loadSongs() {
Log.d(this::class.simpleName, "Starting song search...")

View file

@ -111,7 +111,7 @@ fun NotificationCompat.Builder.updateMode(context: Context) {
if (!NotificationUtils.DO_COMPAT_SUBTEXT) {
val playbackManager = PlaybackStateManager.getInstance()
// If the mode is ALL_SONGS, then just put a string, otherwise put the parent model's name.
// If playing from all songs, set the subtext as that, otherwise the currently played parent.
if (playbackManager.mode == PlaybackMode.ALL_SONGS) {
setSubText(context.getString(R.string.title_all_songs))
} else {

View file

@ -11,6 +11,7 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.playback.queue.QueueAdapter
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager
@ -156,11 +157,13 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
}
// Remove a queue OR user queue item, given a QueueAdapter index.
fun removeQueueItem(adapterIndex: Int) {
fun removeQueueItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
var index = adapterIndex.dec()
// If the item is in the user queue, then remove it from there after accounting for the header.
if (index < mUserQueue.value!!.size) {
queueAdapter.removeItem(adapterIndex)
playbackManager.removeUserQueueItem(index)
} else {
// Translate the indices into proper queue indices if removing an item from there.
@ -170,12 +173,14 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
index -= mUserQueue.value!!.size.inc()
}
queueAdapter.removeItem(adapterIndex)
playbackManager.removeQueueItem(index)
}
}
// Move queue OR user queue items, given QueueAdapter indices.
fun moveQueueItems(adapterFrom: Int, adapterTo: Int): Boolean {
fun moveQueueItems(adapterFrom: Int, adapterTo: Int, queueAdapter: QueueAdapter): Boolean {
var from = adapterFrom.dec()
var to = adapterTo.dec()
@ -183,6 +188,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// Ignore invalid movements to out of bounds, header, or queue positions
if (to >= mUserQueue.value!!.size || to < 0) return false
queueAdapter.moveItems(adapterFrom, adapterTo)
playbackManager.moveUserQueueItems(from, to)
} else {
// Ignore invalid movements to out of bounds or header positions
@ -205,6 +212,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
if (to <= mIndex.value!!) return false
}
queueAdapter.moveItems(adapterFrom, adapterTo)
playbackManager.moveQueueItems(from, to)
}

View file

@ -5,8 +5,8 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.BaseModel
@ -16,11 +16,24 @@ import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
/**
* The single adapter for both the Next Queue and the User Queue.
* - [submitList] is for the plain async diff calculations, use this if you
* have no idea what the differences are between the old data & the new data
* - [removeItem] and [moveItems] are used by [org.oxycblt.auxio.playback.PlaybackViewModel]
* so that this adapter doesn't flip-out when items are moved (Which happens with [AsyncListDiffer])
* @author OxygenCobalt
*/
class QueueAdapter(
val touchHelper: ItemTouchHelper
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
private val touchHelper: ItemTouchHelper
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var data = mutableListOf<BaseModel>()
private var listDiffer = AsyncListDiffer(this, DiffCallback())
override fun getItemCount(): Int = data.size
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
val item = data[position]
return if (item is Header)
HeaderViewHolder.ITEM_TYPE
@ -39,7 +52,7 @@ class QueueAdapter(
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = getItem(position)) {
when (val item = data[position]) {
is Song -> (holder as ViewHolder).bind(item)
is Header -> (holder as HeaderViewHolder).bind(item)
@ -49,6 +62,27 @@ class QueueAdapter(
}
}
fun submitList(newData: MutableList<BaseModel>) {
if (data != newData) {
data = newData
listDiffer.submitList(newData)
}
}
fun moveItems(adapterFrom: Int, adapterTo: Int) {
val item = data.removeAt(adapterFrom)
data.add(adapterTo, item)
notifyItemMoved(adapterFrom, adapterTo)
}
fun removeItem(adapterIndex: Int) {
data.removeAt(adapterIndex)
notifyItemRemoved(adapterIndex)
}
// Generic ViewHolder for a queue item
inner class ViewHolder(
private val binding: ItemQueueSongBinding,

View file

@ -10,6 +10,8 @@ import kotlin.math.sign
// The drag callback used for the Queue RecyclerView.
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
private lateinit var queueAdapter: QueueAdapter
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
@ -51,11 +53,15 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition)
return playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition, queueAdapter)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
playbackModel.removeQueueItem(viewHolder.adapterPosition)
playbackModel.removeQueueItem(viewHolder.adapterPosition, queueAdapter)
}
fun addQueueAdapter(adapter: QueueAdapter) {
queueAdapter = adapter
}
companion object {

View file

@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.BaseModel
@ -27,8 +26,10 @@ class QueueFragment : Fragment() {
): View? {
val binding = FragmentQueueBinding.inflate(inflater)
val helper = ItemTouchHelper(QueueDragCallback(playbackModel))
val callback = QueueDragCallback(playbackModel)
val helper = ItemTouchHelper(callback)
val queueAdapter = QueueAdapter(helper)
callback.addQueueAdapter(queueAdapter)
// --- UI SETUP ---
@ -50,9 +51,7 @@ class QueueFragment : Fragment() {
findNavController().navigateUp()
}
queueAdapter.submitList(createQueueDisplay()) {
scrollRecyclerIfNeeded(binding)
}
queueAdapter.submitList(createQueueData())
}
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
@ -60,15 +59,13 @@ class QueueFragment : Fragment() {
findNavController().navigateUp()
}
queueAdapter.submitList(createQueueDisplay()) {
scrollRecyclerIfNeeded(binding)
}
queueAdapter.submitList(createQueueData())
}
return binding.root
}
private fun createQueueDisplay(): MutableList<BaseModel> {
private fun createQueueData(): MutableList<BaseModel> {
val queue = mutableListOf<BaseModel>()
if (playbackModel.userQueue.value!!.isNotEmpty()) {
@ -93,12 +90,4 @@ class QueueFragment : Fragment() {
return queue
}
private fun scrollRecyclerIfNeeded(binding: FragmentQueueBinding) {
if ((binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstVisibleItemPosition() < 1
) {
binding.queueRecycler.scrollToPosition(0)
}
}
}

View file

@ -13,7 +13,8 @@ import kotlin.random.Random
/**
* Master class for the playback state. This should ***not*** be used outside of the playback module.
* - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel].
* - If you want to add to the system aspects or the exoplayer instance, use [org.oxycblt.auxio.playback.PlaybackService].
* - If you want to use the playback state with the ExoPlayer instance or system-side things,
* use [org.oxycblt.auxio.playback.PlaybackService].
*
* All instantiation should be done with [PlaybackStateManager.from()].
* @author OxygenCobalt
@ -120,7 +121,7 @@ class PlaybackStateManager private constructor() {
PlaybackMode.ALL_SONGS -> null
PlaybackMode.IN_ARTIST -> song.album.artist
PlaybackMode.IN_ALBUM -> song.album
PlaybackMode.IN_GENRE -> error("what")
PlaybackMode.IN_GENRE -> song.album.artist.genres[0]
}
mMode = mode
@ -129,7 +130,7 @@ class PlaybackStateManager private constructor() {
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
PlaybackMode.IN_ARTIST -> song.album.artist.songs
PlaybackMode.IN_ALBUM -> song.album.songs
PlaybackMode.IN_GENRE -> error("what")
PlaybackMode.IN_GENRE -> song.album.artist.genres[0].songs
}
resetLoopMode()

View file

@ -6,10 +6,10 @@ import org.oxycblt.auxio.music.BaseModel
// Base Diff callback
class DiffCallback<T : BaseModel> : DiffUtil.ItemCallback<T>() {
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.id == newItem.id
return oldItem == newItem
}
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem == newItem
return oldItem.id == newItem.id
}
}

View file

@ -39,6 +39,10 @@ fun ImageButton.disable(context: Context) {
}
}
fun String.createToast(context: Context) {
Toast.makeText(context, this, Toast.LENGTH_SHORT).show()
}
// Apply a custom vertical divider
fun RecyclerView.applyDivider() {
val div = DividerItemDecoration(
@ -117,19 +121,7 @@ private fun doUserQueueAdd(context: Context, song: Song, playbackModel: Playback
// This is just to prevent a bug with DiffCallback that creates strange
// behavior when duplicate user queue items are added.
// FIXME: Fix the duplicate item DiffCallback issue
if (!playbackModel.userQueue.value!!.contains(song)) {
playbackModel.addToUserQueue(song)
Toast.makeText(
context,
context.getString(R.string.label_queue_added),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
context.getString(R.string.label_queue_already_added),
Toast.LENGTH_SHORT
).show()
}
context.getString(R.string.label_queue_added).createToast(context)
}

View file

@ -19,7 +19,7 @@ private val ACCENTS = listOf(
Pair(R.color.deep_purple, R.style.Theme_DeepPurple), // 3
Pair(R.color.indigo, R.style.Theme_Indigo), // 4
Pair(R.color.blue, R.style.Theme_Blue), // 5
Pair(R.color.light_blue, R.style.Theme_Blue), // 6
Pair(R.color.light_blue, R.style.Theme_LightBlue), // 6
Pair(R.color.cyan, R.style.Theme_Cyan), // 7
Pair(R.color.teal, R.style.Theme_Teal), // 8
Pair(R.color.green, R.style.Theme_Green), // 9

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="?attr/colorPrimary" />
</shape>
</item>
</selector>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".recycler.viewholders.HeaderViewHolder">
<data>

View file

@ -61,7 +61,6 @@
<string name="placeholder_genre">Unknown Genre</string>
<string name="placeholder_artist">Unknown Artist</string>
<string name="placeholder_album">Unknown Album</string>
<string name="placeholder_song">Unknown Song</string>
<string name="placeholder_no_date">No Date</string>
<!-- Format Namespace | Value formatting/plurals -->

View file

@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"