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" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application <application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.music.processing package org.oxycblt.auxio.music.processing
import android.annotation.SuppressLint
import android.content.ContentResolver import android.content.ContentResolver
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.Audio.Albums import android.provider.MediaStore.Audio.Albums
@ -20,6 +21,7 @@ enum class MusicLoaderResponse {
// Class that loads music from the FileSystem. // Class that loads music from the FileSystem.
// TODO: Add custom artist images from the filesystem // TODO: Add custom artist images from the filesystem
// TODO: Move genre loading of songs [Loads would take longer though]
class MusicLoader( class MusicLoader(
private val resolver: ContentResolver, private val resolver: ContentResolver,
@ -163,9 +165,9 @@ class MusicLoader(
it.genres.add(genre) it.genres.add(genre)
} }
} }
cursor.close()
} }
artistGenreCursor?.close()
} }
Log.d( Log.d(
@ -174,6 +176,7 @@ class MusicLoader(
) )
} }
@SuppressLint("InlinedApi")
private fun loadAlbums() { private fun loadAlbums() {
Log.d(this::class.simpleName, "Starting album search...") Log.d(this::class.simpleName, "Starting album search...")
@ -225,6 +228,7 @@ class MusicLoader(
) )
} }
@SuppressLint("InlinedApi")
private fun loadSongs() { private fun loadSongs() {
Log.d(this::class.simpleName, "Starting song search...") 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) { if (!NotificationUtils.DO_COMPAT_SUBTEXT) {
val playbackManager = PlaybackStateManager.getInstance() 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) { if (playbackManager.mode == PlaybackMode.ALL_SONGS) {
setSubText(context.getString(R.string.title_all_songs)) setSubText(context.getString(R.string.title_all_songs))
} else { } else {

View file

@ -11,6 +11,7 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration 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.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager 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. // 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() 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.
if (index < mUserQueue.value!!.size) { if (index < mUserQueue.value!!.size) {
queueAdapter.removeItem(adapterIndex)
playbackManager.removeUserQueueItem(index) playbackManager.removeUserQueueItem(index)
} else { } else {
// Translate the indices into proper queue indices if removing an item from there. // 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() index -= mUserQueue.value!!.size.inc()
} }
queueAdapter.removeItem(adapterIndex)
playbackManager.removeQueueItem(index) playbackManager.removeQueueItem(index)
} }
} }
// Move queue OR user queue items, given QueueAdapter indices. // 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 from = adapterFrom.dec()
var to = adapterTo.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 // Ignore invalid movements to out of bounds, header, or queue positions
if (to >= mUserQueue.value!!.size || to < 0) return false if (to >= mUserQueue.value!!.size || to < 0) return false
queueAdapter.moveItems(adapterFrom, adapterTo)
playbackManager.moveUserQueueItems(from, to) playbackManager.moveUserQueueItems(from, to)
} else { } else {
// Ignore invalid movements to out of bounds or header positions // 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 if (to <= mIndex.value!!) return false
} }
queueAdapter.moveItems(adapterFrom, adapterTo)
playbackManager.moveQueueItems(from, to) playbackManager.moveQueueItems(from, to)
} }

View file

@ -5,8 +5,8 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.BaseModel 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.BaseViewHolder
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder 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( class QueueAdapter(
val touchHelper: ItemTouchHelper private val touchHelper: ItemTouchHelper
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) { ) : 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 { override fun getItemViewType(position: Int): Int {
val item = getItem(position) val item = data[position]
return if (item is Header) return if (item is Header)
HeaderViewHolder.ITEM_TYPE HeaderViewHolder.ITEM_TYPE
@ -39,7 +52,7 @@ class QueueAdapter(
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 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 Song -> (holder as ViewHolder).bind(item)
is Header -> (holder as HeaderViewHolder).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 // Generic ViewHolder for a queue item
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemQueueSongBinding, private val binding: ItemQueueSongBinding,

View file

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

View file

@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
@ -27,8 +26,10 @@ class QueueFragment : Fragment() {
): View? { ): View? {
val binding = FragmentQueueBinding.inflate(inflater) val binding = FragmentQueueBinding.inflate(inflater)
val helper = ItemTouchHelper(QueueDragCallback(playbackModel)) val callback = QueueDragCallback(playbackModel)
val helper = ItemTouchHelper(callback)
val queueAdapter = QueueAdapter(helper) val queueAdapter = QueueAdapter(helper)
callback.addQueueAdapter(queueAdapter)
// --- UI SETUP --- // --- UI SETUP ---
@ -50,9 +51,7 @@ class QueueFragment : Fragment() {
findNavController().navigateUp() findNavController().navigateUp()
} }
queueAdapter.submitList(createQueueDisplay()) { queueAdapter.submitList(createQueueData())
scrollRecyclerIfNeeded(binding)
}
} }
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
@ -60,15 +59,13 @@ class QueueFragment : Fragment() {
findNavController().navigateUp() findNavController().navigateUp()
} }
queueAdapter.submitList(createQueueDisplay()) { queueAdapter.submitList(createQueueData())
scrollRecyclerIfNeeded(binding)
}
} }
return binding.root return binding.root
} }
private fun createQueueDisplay(): MutableList<BaseModel> { private fun createQueueData(): MutableList<BaseModel> {
val queue = mutableListOf<BaseModel>() val queue = mutableListOf<BaseModel>()
if (playbackModel.userQueue.value!!.isNotEmpty()) { if (playbackModel.userQueue.value!!.isNotEmpty()) {
@ -93,12 +90,4 @@ class QueueFragment : Fragment() {
return queue 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. * 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 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()]. * All instantiation should be done with [PlaybackStateManager.from()].
* @author OxygenCobalt * @author OxygenCobalt
@ -120,7 +121,7 @@ class PlaybackStateManager private constructor() {
PlaybackMode.ALL_SONGS -> null PlaybackMode.ALL_SONGS -> null
PlaybackMode.IN_ARTIST -> song.album.artist PlaybackMode.IN_ARTIST -> song.album.artist
PlaybackMode.IN_ALBUM -> song.album PlaybackMode.IN_ALBUM -> song.album
PlaybackMode.IN_GENRE -> error("what") PlaybackMode.IN_GENRE -> song.album.artist.genres[0]
} }
mMode = mode mMode = mode
@ -129,7 +130,7 @@ class PlaybackStateManager private constructor() {
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList() PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
PlaybackMode.IN_ARTIST -> song.album.artist.songs PlaybackMode.IN_ARTIST -> song.album.artist.songs
PlaybackMode.IN_ALBUM -> song.album.songs PlaybackMode.IN_ALBUM -> song.album.songs
PlaybackMode.IN_GENRE -> error("what") PlaybackMode.IN_GENRE -> song.album.artist.genres[0].songs
} }
resetLoopMode() resetLoopMode()

View file

@ -6,10 +6,10 @@ import org.oxycblt.auxio.music.BaseModel
// Base Diff callback // Base Diff callback
class DiffCallback<T : BaseModel> : DiffUtil.ItemCallback<T>() { class DiffCallback<T : BaseModel> : DiffUtil.ItemCallback<T>() {
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean { override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.id == newItem.id return oldItem == newItem
} }
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { 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 // Apply a custom vertical divider
fun RecyclerView.applyDivider() { fun RecyclerView.applyDivider() {
val div = DividerItemDecoration( 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 // This is just to prevent a bug with DiffCallback that creates strange
// behavior when duplicate user queue items are added. // behavior when duplicate user queue items are added.
// FIXME: Fix the duplicate item DiffCallback issue // FIXME: Fix the duplicate item DiffCallback issue
if (!playbackModel.userQueue.value!!.contains(song)) { playbackModel.addToUserQueue(song)
playbackModel.addToUserQueue(song)
Toast.makeText( context.getString(R.string.label_queue_added).createToast(context)
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()
}
} }

View file

@ -19,7 +19,7 @@ private val ACCENTS = listOf(
Pair(R.color.deep_purple, R.style.Theme_DeepPurple), // 3 Pair(R.color.deep_purple, R.style.Theme_DeepPurple), // 3
Pair(R.color.indigo, R.style.Theme_Indigo), // 4 Pair(R.color.indigo, R.style.Theme_Indigo), // 4
Pair(R.color.blue, R.style.Theme_Blue), // 5 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.cyan, R.style.Theme_Cyan), // 7
Pair(R.color.teal, R.style.Theme_Teal), // 8 Pair(R.color.teal, R.style.Theme_Teal), // 8
Pair(R.color.green, R.style.Theme_Green), // 9 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"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".recycler.viewholders.HeaderViewHolder"> tools:context=".recycler.viewholders.HeaderViewHolder">
<data> <data>

View file

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

View file

@ -7,7 +7,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { 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 "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0" classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"