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"> + +