diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24adc7035..b0a4f913c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
- Added ability to edit previously played or currently playing items in the queue
- Added support for date values formatted as "YYYYMMDD"
- Pressing the button will now clear the current selection before navigating back
+- Added support for non-standard `ARTISTS` tags
#### What's Fixed
- Fixed unreliable ReplayGain adjustment application in certain situations
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1c35add27..81daa45e1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,7 +21,7 @@
InternalPlayer.Action.Open(intent.data ?: return false)
- AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
+ Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
else -> return false
}
playbackModel.startAction(action)
diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
index 67c80091c..794354aef 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
@@ -235,8 +235,8 @@ class MainFragment :
tryHideAllSheets()
}
- // Since the listener is also reliant on the bottom sheets, we must also update it
- // every frame.
+ // Since the navigation listener is also reliant on the bottom sheets, we must also update
+ // it every frame.
callback.invalidateEnabled()
return true
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 bbc6d07ee..c4eccd9c9 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
@@ -82,7 +82,7 @@ class AlbumDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setAlbumUid(args.albumUid)
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
- collectImmediately(detailModel.albumList, detailAdapter::submitList)
+ collectImmediately(detailModel.albumList, detailAdapter::diffList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@@ -170,10 +170,10 @@ class AlbumDetailFragment :
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
- detailAdapter.setPlayingItem(song, isPlaying)
+ detailAdapter.setPlaying(song, isPlaying)
} else {
// Clear the ViewHolders if the mode isn't ALL_SONGS
- detailAdapter.setPlayingItem(null, isPlaying)
+ detailAdapter.setPlaying(null, isPlaying)
}
}
@@ -258,7 +258,7 @@ class AlbumDetailFragment :
}
private fun updateSelection(selected: List) {
- detailAdapter.setSelectedItems(selected)
+ detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
}
}
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 66d25fe08..5d8c0f806 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
@@ -85,7 +85,7 @@ class ArtistDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setArtistUid(args.artistUid)
collectImmediately(detailModel.currentArtist, ::updateItem)
- collectImmediately(detailModel.artistList, detailAdapter::submitList)
+ collectImmediately(detailModel.artistList, detailAdapter::diffList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@@ -195,7 +195,7 @@ class ArtistDetailFragment :
else -> null
}
- detailAdapter.setPlayingItem(playingItem, isPlaying)
+ detailAdapter.setPlaying(playingItem, isPlaying)
}
private fun handleNavigation(item: Music?) {
@@ -234,7 +234,7 @@ class ArtistDetailFragment :
}
private fun updateSelection(selected: List) {
- detailAdapter.setSelectedItems(selected)
+ detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
}
}
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 e72e2753c..b5701fcf6 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
@@ -84,7 +84,7 @@ class GenreDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setGenreUid(args.genreUid)
collectImmediately(detailModel.currentGenre, ::updateItem)
- collectImmediately(detailModel.genreList, detailAdapter::submitList)
+ collectImmediately(detailModel.genreList, detailAdapter::diffList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@@ -189,7 +189,7 @@ class GenreDetailFragment :
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
playingMusic = song
}
- detailAdapter.setPlayingItem(playingMusic, isPlaying)
+ detailAdapter.setPlaying(playingMusic, isPlaying)
}
private fun handleNavigation(item: Music?) {
@@ -217,7 +217,7 @@ class GenreDetailFragment :
}
private fun updateSelection(selected: List) {
- detailAdapter.setSelectedItems(selected)
+ detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt
index d6855c09f..6693ca754 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt
@@ -57,7 +57,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
}
override fun getItemViewType(position: Int) =
- when (differ.currentList[position]) {
+ when (getItem(position)) {
// Support the Album header, sub-headers for each disc, and special album songs.
is Album -> AlbumDetailViewHolder.VIEW_TYPE
is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
@@ -75,7 +75,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
- when (val item = differ.currentList[position]) {
+ when (val item = getItem(position)) {
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
@@ -83,9 +83,12 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
}
override fun isItemFullWidth(position: Int): Boolean {
+ if (super.isItemFullWidth(position)) {
+ return true
+ }
// The album and disc headers should be full-width in all configurations.
- val item = differ.currentList[position]
- return super.isItemFullWidth(position) || item is Album || item is DiscHeader
+ val item = getItem(position)
+ return item is Album || item is DiscHeader
}
private companion object {
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt
index ceb7e9660..29c994c65 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt
@@ -46,7 +46,7 @@ import org.oxycblt.auxio.util.inflater
class ArtistDetailAdapter(private val listener: Listener) :
DetailAdapter(listener, DIFF_CALLBACK) {
override fun getItemViewType(position: Int) =
- when (differ.currentList[position]) {
+ when (getItem(position)) {
// Support an artist header, and special artist albums/songs.
is Artist -> ArtistDetailViewHolder.VIEW_TYPE
is Album -> ArtistAlbumViewHolder.VIEW_TYPE
@@ -65,7 +65,7 @@ class ArtistDetailAdapter(private val listener: Listener) :
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
// Re-binding an item with new data and not just a changed selection/playing state.
- when (val item = differ.currentList[position]) {
+ when (val item = getItem(position)) {
is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener)
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
@@ -73,9 +73,11 @@ class ArtistDetailAdapter(private val listener: Listener) :
}
override fun isItemFullWidth(position: Int): Boolean {
+ if (super.isItemFullWidth(position)) {
+ return true
+ }
// Artist headers should be full-width in all configurations.
- val item = differ.currentList[position]
- return super.isItemFullWidth(position) || item is Artist
+ return getItem(position) is Artist
}
private companion object {
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt
index 7a365b58a..789ca2d19 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt
@@ -20,7 +20,6 @@ package org.oxycblt.auxio.detail.recycler
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
-import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
@@ -37,19 +36,19 @@ import org.oxycblt.auxio.util.inflater
/**
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
* @param listener A [Listener] to bind interactions to.
- * @param itemCallback A [DiffUtil.ItemCallback] to use with [AsyncListDiffer] when updating the
+ * @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class DetailAdapter(
private val listener: Listener<*>,
- itemCallback: DiffUtil.ItemCallback-
-) : SelectionIndicatorAdapter(), AuxioRecyclerView.SpanSizeLookup {
- // Safe to leak this since the listener will not fire during initialization
- @Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, itemCallback)
+ diffCallback: DiffUtil.ItemCallback
-
+) :
+ SelectionIndicatorAdapter
- (ListDiffer.Async(diffCallback)),
+ AuxioRecyclerView.SpanSizeLookup {
override fun getItemViewType(position: Int) =
- when (differ.currentList[position]) {
+ when (getItem(position)) {
// Implement support for headers and sort headers
is Header -> HeaderViewHolder.VIEW_TYPE
is SortHeader -> SortHeaderViewHolder.VIEW_TYPE
@@ -64,7 +63,7 @@ abstract class DetailAdapter(
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
- when (val item = differ.currentList[position]) {
+ when (val item = getItem(position)) {
is Header -> (holder as HeaderViewHolder).bind(item)
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
}
@@ -72,22 +71,10 @@ abstract class DetailAdapter(
override fun isItemFullWidth(position: Int): Boolean {
// Headers should be full-width in all configurations.
- val item = differ.currentList[position]
+ val item = getItem(position)
return item is Header || item is SortHeader
}
- override val currentList: List
-
- get() = differ.currentList
-
- /**
- * Asynchronously update the list with new items. Assumes that the list only contains data
- * supported by the concrete [DetailAdapter] implementation.
- * @param newList The new [Item]s for the adapter to display.
- */
- fun submitList(newList: List
- ) {
- differ.submitList(newList)
- }
-
/** An extended [SelectableListListener] for [DetailAdapter] implementations. */
interface Listener : SelectableListListener {
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented.
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt
index e956c5a91..93eed81c7 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt
@@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.inflater
class GenreDetailAdapter(private val listener: Listener) :
DetailAdapter(listener, DIFF_CALLBACK) {
override fun getItemViewType(position: Int) =
- when (differ.currentList[position]) {
+ when (getItem(position)) {
// Support the Genre header and generic Artist/Song items. There's nothing about
// a genre that will make the artists/songs homogeneous, so it doesn't matter what we
// use for their ViewHolders.
@@ -64,7 +64,7 @@ class GenreDetailAdapter(private val listener: Listener) :
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
- when (val item = differ.currentList[position]) {
+ when (val item = getItem(position)) {
is Genre -> (holder as GenreDetailViewHolder).bind(item, listener)
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
is Song -> (holder as SongViewHolder).bind(item, listener)
@@ -72,9 +72,11 @@ class GenreDetailAdapter(private val listener: Listener) :
}
override fun isItemFullWidth(position: Int): Boolean {
+ if (super.isItemFullWidth(position)) {
+ return true
+ }
// Genre headers should be full-width in all configurations
- val item = differ.currentList[position]
- return super.isItemFullWidth(position) || item is Genre
+ return getItem(position) is Genre
}
private companion object {
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt
index 8cae8c396..e8911eea4 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt
@@ -31,8 +31,8 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
+import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
-import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.formatDurationMs
@@ -67,7 +67,7 @@ class AlbumListFragment :
}
collectImmediately(homeModel.albumsList, albumAdapter::replaceList)
- collectImmediately(selectionModel.selected, albumAdapter::setSelectedItems)
+ collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -130,9 +130,13 @@ class AlbumListFragment :
openMusicMenu(anchor, R.menu.menu_album_actions, item)
}
+ private fun updateSelection(selection: List) {
+ albumAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
+ }
+
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If an album is playing, highlight it within this adapter.
- albumAdapter.setPlayingItem(parent as? Album, isPlaying)
+ albumAdapter.setPlaying(parent as? Album, isPlaying)
}
/**
@@ -140,25 +144,14 @@ class AlbumListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class AlbumAdapter(private val listener: SelectableListListener) :
- SelectionIndicatorAdapter() {
- private val differ = SyncListDiffer(this, AlbumViewHolder.DIFF_CALLBACK)
-
- override val currentList: List
-
- get() = differ.currentList
+ SelectionIndicatorAdapter(
+ ListDiffer.Async(AlbumViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
AlbumViewHolder.from(parent)
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
- holder.bind(differ.currentList[position], listener)
- }
-
- /**
- * Asynchronously update the list with new [Album]s.
- * @param newList The new [Album]s for the adapter to display.
- */
- fun replaceList(newList: List) {
- differ.replaceList(newList)
+ holder.bind(getItem(position), listener)
}
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt
index eaa0bfa2d..278f0d835 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt
@@ -29,9 +29,10 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
+import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
-import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.library.Sort
@@ -48,7 +49,7 @@ class ArtistListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- private val homeAdapter = ArtistAdapter(this)
+ private val artistAdapter = ArtistAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
@@ -58,13 +59,13 @@ class ArtistListFragment :
binding.homeRecycler.apply {
id = R.id.home_artist_recycler
- adapter = homeAdapter
+ adapter = artistAdapter
popupProvider = this@ArtistListFragment
listener = this@ArtistListFragment
}
- collectImmediately(homeModel.artistsList, homeAdapter::replaceList)
- collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
+ collectImmediately(homeModel.artistsList, artistAdapter::replaceList)
+ collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -107,9 +108,13 @@ class ArtistListFragment :
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
}
+ private fun updateSelection(selection: List) {
+ artistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
+ }
+
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If an artist is playing, highlight it within this adapter.
- homeAdapter.setPlayingItem(parent as? Artist, isPlaying)
+ artistAdapter.setPlaying(parent as? Artist, isPlaying)
}
/**
@@ -117,25 +122,14 @@ class ArtistListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class ArtistAdapter(private val listener: SelectableListListener) :
- SelectionIndicatorAdapter() {
- private val differ = SyncListDiffer(this, ArtistViewHolder.DIFF_CALLBACK)
-
- override val currentList: List
-
- get() = differ.currentList
+ SelectionIndicatorAdapter(
+ ListDiffer.Async(ArtistViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ArtistViewHolder.from(parent)
override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) {
- holder.bind(differ.currentList[position], listener)
- }
-
- /**
- * Asynchronously update the list with new [Artist]s.
- * @param newList The new [Artist]s for the adapter to display.
- */
- fun replaceList(newList: List) {
- differ.replaceList(newList)
+ holder.bind(getItem(position), listener)
}
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt
index d0989bd56..30109b43a 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt
@@ -29,9 +29,10 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.recycler.GenreViewHolder
+import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
-import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.library.Sort
@@ -47,7 +48,7 @@ class GenreListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- private val homeAdapter = GenreAdapter(this)
+ private val genreAdapter = GenreAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
@@ -57,13 +58,13 @@ class GenreListFragment :
binding.homeRecycler.apply {
id = R.id.home_genre_recycler
- adapter = homeAdapter
+ adapter = genreAdapter
popupProvider = this@GenreListFragment
listener = this@GenreListFragment
}
- collectImmediately(homeModel.genresList, homeAdapter::replaceList)
- collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
+ collectImmediately(homeModel.genresList, genreAdapter::replaceList)
+ collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -106,9 +107,13 @@ class GenreListFragment :
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
}
+ private fun updateSelection(selection: List) {
+ genreAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
+ }
+
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If a genre is playing, highlight it within this adapter.
- homeAdapter.setPlayingItem(parent as? Genre, isPlaying)
+ genreAdapter.setPlaying(parent as? Genre, isPlaying)
}
/**
@@ -116,25 +121,13 @@ class GenreListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class GenreAdapter(private val listener: SelectableListListener) :
- SelectionIndicatorAdapter() {
- private val differ = SyncListDiffer(this, GenreViewHolder.DIFF_CALLBACK)
-
- override val currentList: List
-
- get() = differ.currentList
-
+ SelectionIndicatorAdapter(
+ ListDiffer.Async(GenreViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GenreViewHolder.from(parent)
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
- holder.bind(differ.currentList[position], listener)
- }
-
- /**
- * Asynchronously update the list with new [Genre]s.
- * @param newList The new [Genre]s for the adapter to display.
- */
- fun replaceList(newList: List) {
- differ.replaceList(newList)
+ holder.bind(getItem(position), listener)
}
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt
index da42fbd9a..e0ab09b87 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt
@@ -30,9 +30,10 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder
-import org.oxycblt.auxio.list.recycler.SyncListDiffer
+import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
@@ -50,7 +51,7 @@ class SongListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- private val homeAdapter = SongAdapter(this)
+ private val songAdapter = SongAdapter(this)
// Save memory by re-using the same formatter and string builder when creating popup text
private val formatterSb = StringBuilder(64)
private val formatter = Formatter(formatterSb)
@@ -63,13 +64,13 @@ class SongListFragment :
binding.homeRecycler.apply {
id = R.id.home_song_recycler
- adapter = homeAdapter
+ adapter = songAdapter
popupProvider = this@SongListFragment
listener = this@SongListFragment
}
- collectImmediately(homeModel.songLists, homeAdapter::replaceList)
- collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
+ collectImmediately(homeModel.songLists, songAdapter::replaceList)
+ collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -136,12 +137,16 @@ class SongListFragment :
openMusicMenu(anchor, R.menu.menu_song_actions, item)
}
+ private fun updateSelection(selection: List) {
+ songAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
+ }
+
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent == null) {
- homeAdapter.setPlayingItem(song, isPlaying)
+ songAdapter.setPlaying(song, isPlaying)
} else {
// Ignore playback that is not from all songs
- homeAdapter.setPlayingItem(null, isPlaying)
+ songAdapter.setPlaying(null, isPlaying)
}
}
@@ -150,25 +155,14 @@ class SongListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class SongAdapter(private val listener: SelectableListListener) :
- SelectionIndicatorAdapter() {
- private val differ = SyncListDiffer(this, SongViewHolder.DIFF_CALLBACK)
-
- override val currentList: List
-
- get() = differ.currentList
+ SelectionIndicatorAdapter(
+ ListDiffer.Async(SongViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
SongViewHolder.from(parent)
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
- holder.bind(differ.currentList[position], listener)
- }
-
- /**
- * Asynchronously update the list with new [Song]s.
- * @param newList The new [Song]s for the adapter to display.
- */
- fun replaceList(newList: List) {
- differ.replaceList(newList)
+ holder.bind(getItem(position), listener)
}
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/list/UpdateInstructions.kt b/app/src/main/java/org/oxycblt/auxio/list/UpdateInstructions.kt
index 1c741aa56..e6a4868d4 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/UpdateInstructions.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/UpdateInstructions.kt
@@ -1,3 +1,20 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
package org.oxycblt.auxio.list
/**
@@ -12,8 +29,8 @@ enum class UpdateInstructions {
DIFF,
/**
- * Synchronously remove the current list and replace it with a new one. This should be used
- * for large diffs with that would cause erratic scroll behavior or in-efficiency.
+ * Synchronously remove the current list and replace it with a new one. This should be used for
+ * large diffs with that would cause erratic scroll behavior or in-efficiency.
*/
REPLACE
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/DiffAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/DiffAdapter.kt
new file mode 100644
index 000000000..aff5e4d2c
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/DiffAdapter.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.list.recycler
+
+import androidx.recyclerview.widget.RecyclerView
+import org.oxycblt.auxio.list.UpdateInstructions
+
+/**
+ * A [RecyclerView.Adapter] with [ListDiffer] integration.
+ * @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
+ */
+abstract class DiffAdapter(differFactory: ListDiffer.Factory) :
+ RecyclerView.Adapter() {
+ private val differ = differFactory.new(@Suppress("LeakingThis") this)
+
+ final override fun getItemCount() = differ.currentList.size
+
+ /** The current list of [T] items. */
+ val currentList: List
+ get() = differ.currentList
+
+ /**
+ * Get a [T] item at the given position.
+ * @param at The position to get the item at.
+ * @throws IndexOutOfBoundsException If the index is not in the list bounds/
+ */
+ fun getItem(at: Int) = differ.currentList[at]
+
+ /**
+ * Dynamically determine how to update the list based on the given [UpdateInstructions].
+ * @param newList The new list of [T] items to show.
+ * @param instructions The [UpdateInstructions] specifying how to update the list.
+ */
+ fun submitList(newList: List, instructions: UpdateInstructions) {
+ when (instructions) {
+ UpdateInstructions.DIFF -> diffList(newList)
+ UpdateInstructions.REPLACE -> replaceList(newList)
+ }
+ }
+
+ /**
+ * Update this list using [DiffUtil]. This can simplify the work of updating the list, but can
+ * also cause erratic behavior.
+ * @param newList The new list of [T] items to show.
+ * @param onDone Callback that will be invoked when the update is completed, allowing means to
+ * reset the state.
+ */
+ fun diffList(newList: List, onDone: () -> Unit = {}) = differ.diffList(newList, onDone)
+
+ /**
+ * Visually replace the previous list with a new list. This is useful for large diffs that are
+ * too erratic for [diffList].
+ * @param newList The new list of [T] items to show.
+ */
+ fun replaceList(newList: List) = differ.replaceList(newList)
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt
new file mode 100644
index 000000000..5dd924b2f
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.list.recycler
+
+import androidx.recyclerview.widget.AdapterListUpdateCallback
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.AsyncListDiffer
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListUpdateCallback
+import androidx.recyclerview.widget.RecyclerView
+import java.lang.reflect.Field
+import org.oxycblt.auxio.util.lazyReflectedField
+import org.oxycblt.auxio.util.requireIs
+
+/**
+ * List differ wrapper that provides more flexibility regarding the way lists are updated.
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+interface ListDiffer {
+ /** The current list of [T] items. */
+ val currentList: List
+
+ /**
+ * Update this list using [DiffUtil]. This can simplify the work of updating the list, but can
+ * also cause erratic behavior.
+ * @param newList The new list of [T] items to show.
+ * @param onDone Callback that will be invoked when the update is completed, allowing means to
+ * reset the state.
+ */
+ fun diffList(newList: List, onDone: () -> Unit = {})
+
+ /**
+ * Visually replace the previous list with a new list. This is useful for large diffs that are
+ * too erratic for [diffList].
+ * @param newList The new list of [T] items to show.
+ */
+ fun replaceList(newList: List)
+
+ /**
+ * Defines the creation of new [ListDiffer] instances. Allows such [ListDiffer]s to be passed as
+ * arguments without reliance on a `this` [RecyclerView.Adapter].
+ */
+ abstract class Factory {
+ /**
+ * Create a new [ListDiffer] bound to the given [RecyclerView.Adapter].
+ * @param adapter The [RecyclerView.Adapter] to bind to.
+ */
+ abstract fun new(adapter: RecyclerView.Adapter<*>): ListDiffer
+ }
+
+ /**
+ * Update lists on another thread. This is useful when large diffs are likely to occur in this
+ * list that would be exceedingly slow with [Blocking].
+ * @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
+ * internal list.
+ */
+ class Async(private val diffCallback: DiffUtil.ItemCallback) : Factory() {
+ override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer =
+ RealAsyncListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
+ }
+
+ /**
+ * Update lists on the main thread. This is useful when many small, discrete list diffs are
+ * likely to occur that would cause [Async] to get race conditions.
+ * @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
+ * internal list.
+ */
+ class Blocking(private val diffCallback: DiffUtil.ItemCallback) : Factory() {
+ override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer =
+ RealBlockingListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
+ }
+}
+
+private class RealAsyncListDiffer(
+ private val updateCallback: ListUpdateCallback,
+ diffCallback: DiffUtil.ItemCallback
+) : ListDiffer {
+ private val inner =
+ AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build())
+
+ override val currentList: List
+ get() = inner.currentList
+
+ override fun diffList(newList: List, onDone: () -> Unit) {
+ inner.submitList(newList, onDone)
+ }
+
+ override fun replaceList(newList: List) {
+ if (inner.currentList == newList) {
+ // Nothing to do.
+ return
+ }
+ // Do possibly the most idiotic thing possible and mutate the internal differ state
+ // so we don't have to deal with any disjoint list garbage. This should cancel any prior
+ // updates and correctly set up the list values while still allowing for the same
+ // visual animation as the blocking replaceList.
+ val oldListSize = inner.currentList.size
+ ASD_MAX_GENERATION_FIELD.set(inner, requireIs(ASD_MAX_GENERATION_FIELD.get(inner)) + 1)
+ ASD_MUTABLE_LIST_FIELD.set(inner, newList.ifEmpty { null })
+ ASD_READ_ONLY_LIST_FIELD.set(inner, newList)
+ updateCallback.onRemoved(0, oldListSize)
+ updateCallback.onInserted(0, newList.size)
+ }
+
+ private companion object {
+ val ASD_MAX_GENERATION_FIELD: Field by
+ lazyReflectedField(AsyncListDiffer::class, "mMaxScheduledGeneration")
+ val ASD_MUTABLE_LIST_FIELD: Field by lazyReflectedField(AsyncListDiffer::class, "mList")
+ val ASD_READ_ONLY_LIST_FIELD: Field by
+ lazyReflectedField(AsyncListDiffer::class, "mReadOnlyList")
+ }
+}
+
+private class RealBlockingListDiffer(
+ private val updateCallback: ListUpdateCallback,
+ private val diffCallback: DiffUtil.ItemCallback
+) : ListDiffer {
+ override var currentList = listOf()
+
+ override fun diffList(newList: List, onDone: () -> Unit) {
+ if (newList === currentList || newList.isEmpty() && currentList.isEmpty()) {
+ onDone()
+ return
+ }
+
+ if (newList.isEmpty()) {
+ val oldListSize = currentList.size
+ currentList = listOf()
+ updateCallback.onRemoved(0, oldListSize)
+ onDone()
+ return
+ }
+
+ if (currentList.isEmpty()) {
+ currentList = newList
+ updateCallback.onInserted(0, newList.size)
+ onDone()
+ return
+ }
+
+ val oldList = currentList
+ val result =
+ DiffUtil.calculateDiff(
+ object : DiffUtil.Callback() {
+ override fun getOldListSize(): Int {
+ return oldList.size
+ }
+
+ override fun getNewListSize(): Int {
+ return newList.size
+ }
+
+ override fun areItemsTheSame(
+ oldItemPosition: Int,
+ newItemPosition: Int
+ ): Boolean {
+ val oldItem: T? = oldList[oldItemPosition]
+ val newItem: T? = newList[newItemPosition]
+ return if (oldItem != null && newItem != null) {
+ diffCallback.areItemsTheSame(oldItem, newItem)
+ } else {
+ oldItem == null && newItem == null
+ }
+ }
+
+ override fun areContentsTheSame(
+ oldItemPosition: Int,
+ newItemPosition: Int
+ ): Boolean {
+ val oldItem: T? = oldList[oldItemPosition]
+ val newItem: T? = newList[newItemPosition]
+ return if (oldItem != null && newItem != null) {
+ diffCallback.areContentsTheSame(oldItem, newItem)
+ } else if (oldItem == null && newItem == null) {
+ true
+ } else {
+ throw AssertionError()
+ }
+ }
+
+ override fun getChangePayload(
+ oldItemPosition: Int,
+ newItemPosition: Int
+ ): Any? {
+ val oldItem: T? = oldList[oldItemPosition]
+ val newItem: T? = newList[newItemPosition]
+ return if (oldItem != null && newItem != null) {
+ diffCallback.getChangePayload(oldItem, newItem)
+ } else {
+ throw AssertionError()
+ }
+ }
+ })
+
+ currentList = newList
+ result.dispatchUpdatesTo(updateCallback)
+ onDone()
+ }
+
+ override fun replaceList(newList: List) {
+ if (currentList == newList) {
+ // Nothing to do.
+ return
+ }
+
+ diffList(listOf())
+ diffList(newList)
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt
index 9b07c339f..12da3c5d9 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt
@@ -19,33 +19,27 @@ package org.oxycblt.auxio.list.recycler
import android.view.View
import androidx.recyclerview.widget.RecyclerView
-import org.oxycblt.auxio.list.Item
-import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.util.logD
/**
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
+ * @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
* @author Alexander Capehart (OxygenCobalt)
*/
-abstract class PlayingIndicatorAdapter : RecyclerView.Adapter() {
+abstract class PlayingIndicatorAdapter(
+ differFactory: ListDiffer.Factory
+) : DiffAdapter(differFactory) {
// There are actually two states for this adapter:
// - The currently playing item, which is usually marked as "selected" and becomes accented.
// - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is
// marked as "playing" or not.
- private var currentMusic: Music? = null
+ private var currentItem: T? = null
private var isPlaying = false
- /**
- * The current list of the adapter. This is used to update items if the indicator state changes.
- */
- abstract val currentList: List
-
-
- override fun getItemCount() = currentList.size
-
override fun onBindViewHolder(holder: VH, position: Int, payloads: List) {
// Only try to update the playing indicator if the ViewHolder supports it
if (holder is ViewHolder) {
- holder.updatePlayingIndicator(currentList[position] == currentMusic, isPlaying)
+ holder.updatePlayingIndicator(currentList[position] == currentItem, isPlaying)
}
if (payloads.isEmpty()) {
@@ -56,14 +50,14 @@ abstract class PlayingIndicatorAdapter : RecyclerV
}
/**
* Update the currently playing item in the list.
- * @param music The [Music] currently being played, or null if it is not being played.
+ * @param item The [T] currently being played, or null if it is not being played.
* @param isPlaying Whether playback is ongoing or paused.
*/
- fun setPlayingItem(music: Music?, isPlaying: Boolean) {
+ fun setPlaying(item: T?, isPlaying: Boolean) {
var updatedItem = false
- if (currentMusic != music) {
- val oldItem = currentMusic
- currentMusic = music
+ if (currentItem != item) {
+ val oldItem = currentItem
+ currentItem = item
// Remove the playing indicator from the old item
if (oldItem != null) {
@@ -76,8 +70,8 @@ abstract class PlayingIndicatorAdapter : RecyclerV
}
// Enable the playing indicator on the new item
- if (music != null) {
- val pos = currentList.indexOfFirst { it == music }
+ if (item != null) {
+ val pos = currentList.indexOfFirst { it == item }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
} else {
@@ -94,8 +88,8 @@ abstract class PlayingIndicatorAdapter : RecyclerV
// We may have already called notifyItemChanged before when checking
// if the item was being played, so in that case we don't need to
// update again here.
- if (!updatedItem && music != null) {
- val pos = currentList.indexOfFirst { it == music }
+ if (!updatedItem && item != null) {
+ val pos = currentList.indexOfFirst { it == item }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
} else {
diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt
index 64036c7cd..6e402878d 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt
@@ -24,11 +24,13 @@ import org.oxycblt.auxio.music.Music
/**
* A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of
* items.
+ * @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
* @author Alexander Capehart (OxygenCobalt)
*/
-abstract class SelectionIndicatorAdapter :
- PlayingIndicatorAdapter() {
- private var selectedItems = setOf()
+abstract class SelectionIndicatorAdapter(
+ differFactory: ListDiffer.Factory
+) : PlayingIndicatorAdapter(differFactory) {
+ private var selectedItems = setOf()
override fun onBindViewHolder(holder: VH, position: Int, payloads: List) {
super.onBindViewHolder(holder, position, payloads)
@@ -39,9 +41,9 @@ abstract class SelectionIndicatorAdapter :
/**
* Update the list of selected items.
- * @param items A list of selected [Music].
+ * @param items A set of selected [T] items.
*/
- fun setSelectedItems(items: List) {
+ fun setSelected(items: Set) {
val oldSelectedItems = selectedItems
val newSelectedItems = items.toSet()
if (newSelectedItems == oldSelectedItems) {
diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt
index c2faf0eab..1d823faa3 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt
@@ -64,8 +64,7 @@ class MetadataExtractor(
/**
* Returns a flow that parses all [Song.Raw] instances queued by the sub-extractors. This will
* first delegate to the sub-extractors before parsing the metadata itself.
- * @param emit A listener that will be invoked with every new [Song.Raw] instance when they are
- * successfully loaded.
+ * @return A flow of [Song.Raw] instances.
*/
fun extract() = flow {
while (true) {
@@ -310,8 +309,9 @@ class Task(context: Context, private val raw: Song.Raw) {
// Album artist
comments["musicbrainz_albumartistid"]?.let { raw.albumArtistMusicBrainzIds = it }
(comments["albumartists"] ?: comments["albumartist"])?.let { raw.albumArtistNames = it }
- (comments["albumartists_sort"] ?: comments["albumartistsort"])
- ?.let { raw.albumArtistSortNames = it }
+ (comments["albumartists_sort"] ?: comments["albumartistsort"])?.let {
+ raw.albumArtistSortNames = it
+ }
// Genre
comments["genre"]?.let { raw.genreNames = it }
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt
index 9e46e5946..c33e1ce1b 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt
@@ -199,7 +199,7 @@ interface PlaybackSettings : Settings {
}
}
- companion object {
+ private companion object {
const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"
const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode"
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt
index d195b26e9..4922d6d35 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt
@@ -27,9 +27,10 @@ import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.list.EditableListListener
+import org.oxycblt.auxio.list.recycler.DiffAdapter
+import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder
-import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.*
@@ -39,16 +40,13 @@ import org.oxycblt.auxio.util.*
* @author Alexander Capehart (OxygenCobalt)
*/
class QueueAdapter(private val listener: EditableListListener) :
- RecyclerView.Adapter() {
- private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK)
+ DiffAdapter(ListDiffer.Blocking(QueueSongViewHolder.DIFF_CALLBACK)) {
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this
// adapter, as one item can appear at several points in the UI. Use a similar implementation
// with an index value instead.
private var currentIndex = 0
private var isPlaying = false
- override fun getItemCount() = differ.currentList.size
-
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
QueueSongViewHolder.from(parent)
@@ -61,31 +59,13 @@ class QueueAdapter(private val listener: EditableListListener) :
payload: List
) {
if (payload.isEmpty()) {
- viewHolder.bind(differ.currentList[position], listener)
+ viewHolder.bind(getItem(position), listener)
}
viewHolder.isFuture = position > currentIndex
viewHolder.updatePlayingIndicator(position == currentIndex, isPlaying)
}
- /**
- * Synchronously update the list with new items. This is exceedingly slow for large diffs, so
- * only use it for trivial updates.
- * @param newList The new [Song]s for the adapter to display.
- */
- fun submitList(newList: List) {
- differ.submitList(newList)
- }
-
- /**
- * Replace the list with a new list. This is exceedingly slow for large diffs, so only use it
- * for trivial updates.
- * @param newList The new [Song]s for the adapter to display.
- */
- fun replaceList(newList: List) {
- differ.replaceList(newList)
- }
-
/**
* Set the position of the currently playing item in the queue. This will mark the item as
* playing and any previous items as played.
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 879a2879f..3d02e40aa 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
@@ -33,7 +33,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
-import org.oxycblt.auxio.util.logD
/**
* A [ViewBindingFragment] that displays an editable queue.
@@ -102,13 +101,7 @@ class QueueFragment : ViewBindingFragment(), EditableListL
// Replace or diff the queue depending on the type of change it is.
val instructions = queueModel.instructions
- if (instructions?.update == UpdateInstructions.REPLACE) {
- logD("Replacing queue")
- queueAdapter.replaceList(queue)
- } else {
- logD("Diffing queue")
- queueAdapter.submitList(queue)
- }
+ queueAdapter.submitList(queue, instructions?.update ?: UpdateInstructions.DIFF)
// Update position in list (and thus past/future items)
queueAdapter.setPosition(index, isPlaying)
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt
index 1fbbde002..717750dbf 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt
@@ -165,7 +165,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
fun write(state: SavedState?) {
requireBackgroundThread()
// Only bother saving a state if a song is actively playing from one.
- // This is not the case with a null state or a state with an out-of-bounds index.
+ // This is not the case with a null state.
if (state != null) {
// Transform saved state into raw state, which can then be written to the database.
val rawPlaybackState =
diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt
index 6653b8e24..edc8d90df 100644
--- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt
@@ -18,7 +18,6 @@
package org.oxycblt.auxio.search
import android.view.ViewGroup
-import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.recycler.*
@@ -30,14 +29,11 @@ import org.oxycblt.auxio.music.*
* @author Alexander Capehart (OxygenCobalt)
*/
class SearchAdapter(private val listener: SelectableListListener) :
- SelectionIndicatorAdapter(), AuxioRecyclerView.SpanSizeLookup {
- private val differ = AsyncListDiffer(this, DIFF_CALLBACK)
-
- override val currentList: List
-
- get() = differ.currentList
+ SelectionIndicatorAdapter
- (ListDiffer.Async(DIFF_CALLBACK)),
+ AuxioRecyclerView.SpanSizeLookup {
override fun getItemViewType(position: Int) =
- when (differ.currentList[position]) {
+ when (getItem(position)) {
is Song -> SongViewHolder.VIEW_TYPE
is Album -> AlbumViewHolder.VIEW_TYPE
is Artist -> ArtistViewHolder.VIEW_TYPE
@@ -57,7 +53,7 @@ class SearchAdapter(private val listener: SelectableListListener) :
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
- when (val item = differ.currentList[position]) {
+ when (val item = getItem(position)) {
is Song -> (holder as SongViewHolder).bind(item, listener)
is Album -> (holder as AlbumViewHolder).bind(item, listener)
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
@@ -66,17 +62,7 @@ class SearchAdapter(private val listener: SelectableListListener) :
}
}
- override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header
-
- /**
- * Asynchronously update the list with new items. Assumes that the list only contains supported
- * data..
- * @param newList The new [Item]s for the adapter to display.
- * @param callback A block called when the asynchronous update is completed.
- */
- fun submitList(newList: List
- , callback: () -> Unit) {
- differ.submitList(newList, callback)
- }
+ override fun isItemFullWidth(position: Int) = getItem(position) is Header
private companion object {
/** A comparator that can be used with DiffUtil. */
diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt
index 2c837a888..1fe364acb 100644
--- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt
@@ -153,7 +153,7 @@ class SearchFragment : ListFragment() {
// Don't show the RecyclerView (and it's stray overscroll effects) when there
// are no results.
binding.searchRecycler.isInvisible = results.isEmpty()
- searchAdapter.submitList(results.toMutableList()) {
+ searchAdapter.diffList(results.toMutableList()) {
// I would make it so that the position is only scrolled back to the top when
// the query actually changes instead of once every re-creation event, but sadly
// that doesn't seem possible.
@@ -162,7 +162,7 @@ class SearchFragment : ListFragment() {
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
- searchAdapter.setPlayingItem(parent ?: song, isPlaying)
+ searchAdapter.setPlaying(parent ?: song, isPlaying)
}
private fun handleNavigation(item: Music?) {
@@ -180,7 +180,7 @@ class SearchFragment : ListFragment() {
}
private fun updateSelection(selected: List) {
- searchAdapter.setSelectedItems(selected)
+ searchAdapter.setSelected(selected.toSet())
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
selected.isNotEmpty()) {
// Make selection of obscured items easier by hiding the keyboard.
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt
index bd6dfe9f3..aa94552d8 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt
@@ -123,14 +123,15 @@ class AboutFragment : ViewBindingFragment() {
if (pkgName == "android") {
// No default browser [Must open app chooser, may not be supported]
openAppChooser(browserIntent)
- } else try {
- browserIntent.setPackage(pkgName)
- startActivity(browserIntent)
- } catch (e: ActivityNotFoundException) {
- // Not a browser but an app chooser
- browserIntent.setPackage(null)
- openAppChooser(browserIntent)
- }
+ } else
+ try {
+ browserIntent.setPackage(pkgName)
+ startActivity(browserIntent)
+ } catch (e: ActivityNotFoundException) {
+ // Not a browser but an app chooser
+ browserIntent.setPackage(null)
+ openAppChooser(browserIntent)
+ }
} else {
// No app installed to open the link
context.showToast(R.string.err_no_app)
diff --git a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt
index dedc5efc4..5e9de4b83 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt
@@ -75,10 +75,8 @@ interface UISettings : Settings {
var accent = sharedPreferences.getInt(OLD_KEY_ACCENT3, 5)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Accents were previously frozen as soon as the OS was updated to android
- // twelve,
- // as dynamic colors were enabled by default. This is no longer the case, so we
- // need
- // to re-update the setting to dynamic colors here.
+ // twelve, as dynamic colors were enabled by default. This is no longer the
+ // case, so we need to re-update the setting to dynamic colors here.
accent = 16
}
@@ -96,7 +94,7 @@ interface UISettings : Settings {
}
}
- companion object {
+ private companion object {
const val OLD_KEY_ACCENT3 = "auxio_accent"
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt
index dfaca9127..caf2f811e 100644
--- a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt
+++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt
@@ -39,7 +39,8 @@ fun unlikelyToBeNull(value: T?) =
* @return A data casted to [T].
* @throws IllegalStateException If the data cannot be casted to [T].
*/
-inline fun requireIs(data: Any): T {
+inline fun requireIs(data: Any?): T {
+ requireNotNull(data) { "Unexpected datatype: null" }
check(data is T) { "Unexpected datatype: ${data::class.simpleName}" }
return data
}
diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
index abc7469ee..6c22299d1 100644
--- a/app/src/main/res/layout/fragment_main.xml
+++ b/app/src/main/res/layout/fragment_main.xml
@@ -8,6 +8,8 @@
android:background="?attr/colorSurface"
android:transitionGroup="true">
+
+