From 7915655c785234dfd339fe6cbaa4265c426916c2 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 18 May 2023 10:23:34 +0300 Subject: [PATCH 01/43] Songs sharing --- .../org/oxycblt/auxio/list/ListFragment.kt | 4 ++ .../auxio/list/selection/SelectionFragment.kt | 5 ++ .../java/org/oxycblt/auxio/music/fs/Fs.kt | 10 +++ .../java/org/oxycblt/auxio/util/ShareUtil.kt | 63 +++++++++++++++++++ .../main/res/menu/menu_selection_actions.xml | 4 ++ app/src/main/res/menu/menu_song_actions.xml | 3 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 90 insertions(+) create mode 100644 app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index f2e050e40..a17a666b9 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -32,6 +32,7 @@ import org.oxycblt.auxio.music.* import org.oxycblt.auxio.navigation.MainNavigationAction import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.shareSong import org.oxycblt.auxio.util.showToast /** @@ -99,6 +100,9 @@ abstract class ListFragment : R.id.action_go_album -> { navModel.exploreNavigateTo(song.album) } + R.id.action_song_share -> { + requireContext().shareSong(song) + } R.id.action_playlist_add -> { musicModel.addToPlaylist(song) } diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt index bcba5195e..fd43f7653 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt @@ -26,6 +26,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment +import org.oxycblt.auxio.util.shareSongs import org.oxycblt.auxio.util.showToast /** @@ -86,6 +87,10 @@ abstract class SelectionFragment : playbackModel.shuffle(selectionModel.take()) true } + R.id.action_selection_share -> { + requireContext().shareSongs(selectionModel.take()) + true + } else -> false } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt index defbb7c3f..f169f8bde 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt @@ -212,4 +212,14 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) { MimeTypeMap.getSingleton().getExtensionFromMimeType(fromExtension)?.uppercase() } } + + /** + * Return a mime-type such as "audio/ogg" + * + * @return A raw mime-type string. Will first try [fromFormat], then falling + * back to [fromExtension], and then null if that fails. + */ + fun getRawType(): String { + return fromFormat ?: fromExtension + } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt new file mode 100644 index 000000000..05a71ef94 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 Auxio Project + * ShareUtil.kt is part of Auxio. + * + * 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.util + +import android.content.Context +import androidx.core.app.ShareCompat +import org.oxycblt.auxio.music.Song + +private const val MIME_TYPE_FALLBACK = "audio/*" + +/** + * Show system share sheet to share song + * + * @param song the [Song] to share + */ +fun Context.shareSong(song: Song) { + ShareCompat.IntentBuilder(this) + .setStream(song.uri) + .setType(song.mimeType.getRawType()) + .startChooser() +} + +/** + * Show system share sheet to share multiple song + * + * @param songs the collection of [Song] to share + */ +fun Context.shareSongs(songs: Collection) { + if (songs.isEmpty()) { + return + } + if (songs.size == 1) { + shareSong(songs.first()) + return + } + val type = songs.mapTo(HashSet(songs.size)) { + it.mimeType.getRawType() + }.singleOrNull() ?: MIME_TYPE_FALLBACK + ShareCompat.IntentBuilder(this) + .apply { + for (song in songs) { + addStream(song.uri) + } + } + .setType(type) + .startChooser() +} diff --git a/app/src/main/res/menu/menu_selection_actions.xml b/app/src/main/res/menu/menu_selection_actions.xml index 568d04a62..e596b97a6 100644 --- a/app/src/main/res/menu/menu_selection_actions.xml +++ b/app/src/main/res/menu/menu_selection_actions.xml @@ -21,4 +21,8 @@ android:id="@+id/action_selection_shuffle" android:title="@string/lbl_shuffle_selected" app:showAsAction="never"/> + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_song_actions.xml b/app/src/main/res/menu/menu_song_actions.xml index abb176fb5..dbb15382e 100644 --- a/app/src/main/res/menu/menu_song_actions.xml +++ b/app/src/main/res/menu/menu_song_actions.xml @@ -15,6 +15,9 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4e356c7a..f40e849d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -118,6 +118,7 @@ Go to artist Go to album View properties + Share Song properties File name From 7f11e886f7b4e4e15ab8d20d6575f6474ddef917 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 18 May 2023 12:07:00 +0300 Subject: [PATCH 02/43] Sharing albums, artists, genres and playlists --- .../oxycblt/auxio/detail/AlbumDetailFragment.kt | 4 ++++ .../oxycblt/auxio/detail/ArtistDetailFragment.kt | 4 ++++ .../oxycblt/auxio/detail/GenreDetailFragment.kt | 4 ++++ .../auxio/detail/PlaylistDetailFragment.kt | 4 ++++ .../java/org/oxycblt/auxio/list/ListFragment.kt | 15 ++++++++++++++- .../main/java/org/oxycblt/auxio/music/fs/Fs.kt | 4 ++-- .../auxio/playback/PlaybackPanelFragment.kt | 5 +++++ .../main/java/org/oxycblt/auxio/util/ShareUtil.kt | 2 +- app/src/main/res/menu/menu_album_actions.xml | 3 +++ app/src/main/res/menu/menu_album_detail.xml | 3 +++ app/src/main/res/menu/menu_album_song_actions.xml | 3 +++ .../main/res/menu/menu_artist_album_actions.xml | 3 +++ .../main/res/menu/menu_artist_song_actions.xml | 3 +++ app/src/main/res/menu/menu_parent_actions.xml | 3 +++ app/src/main/res/menu/menu_parent_detail.xml | 3 +++ app/src/main/res/menu/menu_playback.xml | 3 +++ app/src/main/res/menu/menu_playlist_actions.xml | 3 +++ app/src/main/res/menu/menu_playlist_detail.xml | 3 +++ app/src/main/res/menu/menu_song_actions.xml | 2 +- 19 files changed, 69 insertions(+), 5 deletions(-) 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 42d6bb341..6e457c43c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -142,6 +142,10 @@ class AlbumDetailFragment : musicModel.addToPlaylist(currentAlbum) true } + R.id.action_share -> { + requireContext().shareSongs(currentAlbum.songs) + true + } else -> false } } 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 4578f664d..0ea42c903 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -137,6 +137,10 @@ class ArtistDetailFragment : musicModel.addToPlaylist(currentArtist) true } + R.id.action_share -> { + requireContext().shareSongs(currentArtist.songs) + true + } else -> false } } 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 2028c3610..403e4109f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -130,6 +130,10 @@ class GenreDetailFragment : musicModel.addToPlaylist(currentGenre) true } + R.id.action_share -> { + requireContext().shareSongs(currentGenre.songs) + true + } else -> false } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index d1214f6b2..d42bd5823 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -130,6 +130,10 @@ class PlaylistDetailFragment : musicModel.deletePlaylist(currentPlaylist) true } + R.id.action_share -> { + requireContext().shareSongs(currentPlaylist.songs) + true + } else -> false } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index a17a666b9..c4763d302 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -33,6 +33,7 @@ import org.oxycblt.auxio.navigation.MainNavigationAction import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.shareSong +import org.oxycblt.auxio.util.shareSongs import org.oxycblt.auxio.util.showToast /** @@ -100,7 +101,7 @@ abstract class ListFragment : R.id.action_go_album -> { navModel.exploreNavigateTo(song.album) } - R.id.action_song_share -> { + R.id.action_share -> { requireContext().shareSong(song) } R.id.action_playlist_add -> { @@ -151,6 +152,9 @@ abstract class ListFragment : R.id.action_playlist_add -> { musicModel.addToPlaylist(album) } + R.id.action_share -> { + requireContext().shareSongs(album.songs) + } else -> { error("Unexpected menu item selected") } @@ -188,6 +192,9 @@ abstract class ListFragment : R.id.action_playlist_add -> { musicModel.addToPlaylist(artist) } + R.id.action_share -> { + requireContext().shareSongs(artist.songs) + } else -> { error("Unexpected menu item selected") } @@ -225,6 +232,9 @@ abstract class ListFragment : R.id.action_playlist_add -> { musicModel.addToPlaylist(genre) } + R.id.action_share -> { + requireContext().shareSongs(genre.songs) + } else -> { error("Unexpected menu item selected") } @@ -262,6 +272,9 @@ abstract class ListFragment : R.id.action_delete -> { musicModel.deletePlaylist(playlist) } + R.id.action_share -> { + requireContext().shareSongs(playlist.songs) + } else -> { error("Unexpected menu item selected") } diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt index f169f8bde..cc5334945 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt @@ -216,8 +216,8 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) { /** * Return a mime-type such as "audio/ogg" * - * @return A raw mime-type string. Will first try [fromFormat], then falling - * back to [fromExtension], and then null if that fails. + * @return A raw mime-type string. Will first try [fromFormat], then falling back to + * [fromExtension], and then null if that fails. */ fun getRawType(): String { return fromFormat ?: fromExtension diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 636e2e2ca..64e3f9c1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -42,6 +42,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.shareSong import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.systemBarInsetsCompat @@ -172,6 +173,10 @@ class PlaybackPanelFragment : } true } + R.id.action_share -> { + playbackModel.song.value?.let { requireContext().shareSong(it) } + true + } else -> false } diff --git a/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt index 05a71ef94..877795afc 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.util import android.content.Context diff --git a/app/src/main/res/menu/menu_album_actions.xml b/app/src/main/res/menu/menu_album_actions.xml index 7292d9016..6f9f28aff 100644 --- a/app/src/main/res/menu/menu_album_actions.xml +++ b/app/src/main/res/menu/menu_album_actions.xml @@ -18,4 +18,7 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_album_detail.xml b/app/src/main/res/menu/menu_album_detail.xml index 4c5d8d7d5..2da5cee9b 100644 --- a/app/src/main/res/menu/menu_album_detail.xml +++ b/app/src/main/res/menu/menu_album_detail.xml @@ -9,4 +9,7 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_album_song_actions.xml b/app/src/main/res/menu/menu_album_song_actions.xml index 256322f3e..d356eeedb 100644 --- a/app/src/main/res/menu/menu_album_song_actions.xml +++ b/app/src/main/res/menu/menu_album_song_actions.xml @@ -12,6 +12,9 @@ + diff --git a/app/src/main/res/menu/menu_artist_album_actions.xml b/app/src/main/res/menu/menu_artist_album_actions.xml index c94d6886f..a39b0127e 100644 --- a/app/src/main/res/menu/menu_artist_album_actions.xml +++ b/app/src/main/res/menu/menu_artist_album_actions.xml @@ -18,4 +18,7 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_artist_song_actions.xml b/app/src/main/res/menu/menu_artist_song_actions.xml index 4b20abd21..aaad4040a 100644 --- a/app/src/main/res/menu/menu_artist_song_actions.xml +++ b/app/src/main/res/menu/menu_artist_song_actions.xml @@ -12,6 +12,9 @@ + diff --git a/app/src/main/res/menu/menu_parent_actions.xml b/app/src/main/res/menu/menu_parent_actions.xml index 4e6112035..b741fd51b 100644 --- a/app/src/main/res/menu/menu_parent_actions.xml +++ b/app/src/main/res/menu/menu_parent_actions.xml @@ -12,6 +12,9 @@ + diff --git a/app/src/main/res/menu/menu_parent_detail.xml b/app/src/main/res/menu/menu_parent_detail.xml index 3a2225ea3..e73829b41 100644 --- a/app/src/main/res/menu/menu_parent_detail.xml +++ b/app/src/main/res/menu/menu_parent_detail.xml @@ -9,4 +9,7 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_playback.xml b/app/src/main/res/menu/menu_playback.xml index fd9e761a0..601b54b68 100644 --- a/app/src/main/res/menu/menu_playback.xml +++ b/app/src/main/res/menu/menu_playback.xml @@ -14,6 +14,9 @@ android:id="@+id/action_go_album" android:title="@string/lbl_go_album" app:showAsAction="never" /> + + diff --git a/app/src/main/res/menu/menu_playlist_detail.xml b/app/src/main/res/menu/menu_playlist_detail.xml index c3e30e8b9..761c42dd5 100644 --- a/app/src/main/res/menu/menu_playlist_detail.xml +++ b/app/src/main/res/menu/menu_playlist_detail.xml @@ -6,6 +6,9 @@ + diff --git a/app/src/main/res/menu/menu_song_actions.xml b/app/src/main/res/menu/menu_song_actions.xml index dbb15382e..cba78013b 100644 --- a/app/src/main/res/menu/menu_song_actions.xml +++ b/app/src/main/res/menu/menu_song_actions.xml @@ -16,7 +16,7 @@ android:id="@+id/action_playlist_add" android:title="@string/lbl_playlist_add" /> Date: Thu, 18 May 2023 19:27:50 +0300 Subject: [PATCH 03/43] Fix style issues --- .../auxio/detail/AlbumDetailFragment.kt | 2 +- .../auxio/detail/ArtistDetailFragment.kt | 2 +- .../auxio/detail/GenreDetailFragment.kt | 2 +- .../auxio/detail/PlaylistDetailFragment.kt | 2 +- .../org/oxycblt/auxio/list/ListFragment.kt | 13 ++-- .../auxio/list/selection/SelectionFragment.kt | 4 +- .../java/org/oxycblt/auxio/music/fs/Fs.kt | 20 +++--- .../auxio/playback/PlaybackPanelFragment.kt | 4 +- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 40 ++++++++++++ .../java/org/oxycblt/auxio/util/ShareUtil.kt | 63 ------------------- 10 files changed, 64 insertions(+), 88 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt 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 6e457c43c..7ed832468 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -143,7 +143,7 @@ class AlbumDetailFragment : true } R.id.action_share -> { - requireContext().shareSongs(currentAlbum.songs) + requireContext().share(currentAlbum) true } else -> false 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 0ea42c903..43e3e6993 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -138,7 +138,7 @@ class ArtistDetailFragment : true } R.id.action_share -> { - requireContext().shareSongs(currentArtist.songs) + requireContext().share(currentArtist) true } else -> false 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 403e4109f..581b2e18d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -131,7 +131,7 @@ class GenreDetailFragment : true } R.id.action_share -> { - requireContext().shareSongs(currentGenre.songs) + requireContext().share(currentGenre) true } else -> false diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index d42bd5823..b1ef26f3e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -131,7 +131,7 @@ class PlaylistDetailFragment : true } R.id.action_share -> { - requireContext().shareSongs(currentPlaylist.songs) + requireContext().share(currentPlaylist) true } else -> false diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index c4763d302..c9fca7b0e 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -32,8 +32,7 @@ import org.oxycblt.auxio.music.* import org.oxycblt.auxio.navigation.MainNavigationAction import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.shareSong -import org.oxycblt.auxio.util.shareSongs +import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast /** @@ -102,7 +101,7 @@ abstract class ListFragment : navModel.exploreNavigateTo(song.album) } R.id.action_share -> { - requireContext().shareSong(song) + requireContext().share(song) } R.id.action_playlist_add -> { musicModel.addToPlaylist(song) @@ -153,7 +152,7 @@ abstract class ListFragment : musicModel.addToPlaylist(album) } R.id.action_share -> { - requireContext().shareSongs(album.songs) + requireContext().share(album) } else -> { error("Unexpected menu item selected") @@ -193,7 +192,7 @@ abstract class ListFragment : musicModel.addToPlaylist(artist) } R.id.action_share -> { - requireContext().shareSongs(artist.songs) + requireContext().share(artist) } else -> { error("Unexpected menu item selected") @@ -233,7 +232,7 @@ abstract class ListFragment : musicModel.addToPlaylist(genre) } R.id.action_share -> { - requireContext().shareSongs(genre.songs) + requireContext().share(genre) } else -> { error("Unexpected menu item selected") @@ -273,7 +272,7 @@ abstract class ListFragment : musicModel.deletePlaylist(playlist) } R.id.action_share -> { - requireContext().shareSongs(playlist.songs) + requireContext().share(playlist) } else -> { error("Unexpected menu item selected") diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt index fd43f7653..2cb2403da 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment -import org.oxycblt.auxio.util.shareSongs +import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast /** @@ -88,7 +88,7 @@ abstract class SelectionFragment : true } R.id.action_selection_share -> { - requireContext().shareSongs(selectionModel.take()) + requireContext().share(selectionModel.take()) true } else -> false diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt index cc5334945..63ef57a0c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt @@ -147,6 +147,16 @@ data class MusicDirectories(val dirs: List, val shouldInclude: Boolea * @author Alexander Capehart (OxygenCobalt) */ data class MimeType(val fromExtension: String, val fromFormat: String?) { + + /** + * Return a mime-type such as "audio/ogg" + * + * @return A raw mime-type string. Will first try [fromFormat], then falling back to + * [fromExtension], and then null if that fails. + */ + val raw: String + get() = fromFormat ?: fromExtension + /** * Resolve the mime type into a human-readable format name, such as "Ogg Vorbis". * @@ -212,14 +222,4 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) { MimeTypeMap.getSingleton().getExtensionFromMimeType(fromExtension)?.uppercase() } } - - /** - * Return a mime-type such as "audio/ogg" - * - * @return A raw mime-type string. Will first try [fromFormat], then falling back to - * [fromExtension], and then null if that fails. - */ - fun getRawType(): String { - return fromFormat ?: fromExtension - } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 64e3f9c1d..b02964c8d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -42,7 +42,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.shareSong +import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.systemBarInsetsCompat @@ -174,7 +174,7 @@ class PlaybackPanelFragment : true } R.id.action_share -> { - playbackModel.song.value?.let { requireContext().shareSong(it) } + playbackModel.song.value?.let { requireContext().share(it) } true } else -> false diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 14cd2a20e..abee5c248 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -27,6 +27,7 @@ import android.view.WindowInsets import androidx.annotation.RequiresApi import androidx.appcompat.widget.AppCompatButton import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.app.ShareCompat import androidx.core.graphics.Insets import androidx.core.graphics.drawable.DrawableCompat import androidx.navigation.NavController @@ -34,6 +35,11 @@ import androidx.navigation.NavDirections import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import java.lang.IllegalArgumentException +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.Song + +private const val MIME_TYPE_FALLBACK = "audio/*" /** * Get if this [View] contains the given [PointF], with optional leeway. @@ -248,3 +254,37 @@ fun WindowInsets.replaceSystemBarInsetsCompat( } } } + +/** + * Show system share sheet to share songs + * + * @param music the [Music] to share + */ +fun Context.share(music: Music) = share( + when (music) { + is Song -> listOf(music) + is MusicParent -> music.songs + } +) + +/** + * Show system share sheet to share multiple song + * + * @param songs the collection of [Song] to share + */ +fun Context.share(songs: Collection) { + if (songs.isEmpty()) { + return + } + val type = songs.mapTo(HashSet(songs.size)) { + it.mimeType.raw + }.singleOrNull() ?: MIME_TYPE_FALLBACK + ShareCompat.IntentBuilder(this) + .apply { + for (song in songs) { + addStream(song.uri) + } + } + .setType(type) + .startChooser() +} diff --git a/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt deleted file mode 100644 index 877795afc..000000000 --- a/app/src/main/java/org/oxycblt/auxio/util/ShareUtil.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * ShareUtil.kt is part of Auxio. - * - * 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.util - -import android.content.Context -import androidx.core.app.ShareCompat -import org.oxycblt.auxio.music.Song - -private const val MIME_TYPE_FALLBACK = "audio/*" - -/** - * Show system share sheet to share song - * - * @param song the [Song] to share - */ -fun Context.shareSong(song: Song) { - ShareCompat.IntentBuilder(this) - .setStream(song.uri) - .setType(song.mimeType.getRawType()) - .startChooser() -} - -/** - * Show system share sheet to share multiple song - * - * @param songs the collection of [Song] to share - */ -fun Context.shareSongs(songs: Collection) { - if (songs.isEmpty()) { - return - } - if (songs.size == 1) { - shareSong(songs.first()) - return - } - val type = songs.mapTo(HashSet(songs.size)) { - it.mimeType.getRawType() - }.singleOrNull() ?: MIME_TYPE_FALLBACK - ShareCompat.IntentBuilder(this) - .apply { - for (song in songs) { - addStream(song.uri) - } - } - .setType(type) - .startChooser() -} From af563c83ac33d0d9076a19371db5a921061b3037 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 22 May 2023 21:05:46 -0600 Subject: [PATCH 04/43] build: reduce execution times Reduce build execution times through the use of non-transitive/final R classes and removing unnecessary wildcard imports. --- .../BackportBottomSheetBehavior.java | 6 ++-- ...BackportMaterialDividerItemDecoration.java | 11 ++++---- .../java/org/oxycblt/auxio/MainFragment.kt | 13 +++++++-- .../auxio/detail/AlbumDetailFragment.kt | 9 +++++- .../auxio/detail/ArtistDetailFragment.kt | 8 +++++- .../oxycblt/auxio/detail/DetailViewModel.kt | 16 +++++++++-- .../auxio/detail/GenreDetailFragment.kt | 16 +++++++++-- .../auxio/detail/PlaylistDetailFragment.kt | 16 +++++++++-- .../oxycblt/auxio/detail/ReadOnlyTextInput.kt | 2 +- .../detail/list/ArtistDetailListAdapter.kt | 5 +++- .../auxio/detail/list/DetailListAdapter.kt | 7 +++-- .../detail/list/PlaylistDetailListAdapter.kt | 5 ++-- .../auxio/detail/list/SongPropertyAdapter.kt | 3 +- .../auxio/home/FlipFloatingActionButton.kt | 2 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 28 +++++++++++++++++-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 9 +++++- .../home/fastscroll/FastScrollPopupView.kt | 8 ++++-- .../home/fastscroll/FastScrollRecyclerView.kt | 7 ++++- .../auxio/home/list/AlbumListFragment.kt | 8 ++++-- .../auxio/home/list/ArtistListFragment.kt | 2 +- .../auxio/home/list/GenreListFragment.kt | 2 +- .../auxio/home/list/PlaylistListFragment.kt | 2 +- .../auxio/home/list/SongListFragment.kt | 2 +- .../org/oxycblt/auxio/image/ImageGroup.kt | 9 ++++-- .../org/oxycblt/auxio/image/ImageModule.kt | 1 - .../oxycblt/auxio/image/StyledImageView.kt | 7 ++++- .../auxio/image/extractor/Components.kt | 2 +- .../org/oxycblt/auxio/list/ListFragment.kt | 7 ++++- .../main/java/org/oxycblt/auxio/list/Sort.kt | 9 +++++- .../auxio/list/adapter/FlexibleListAdapter.kt | 3 +- .../auxio/list/recycler/DialogRecyclerView.kt | 1 + .../list/recycler/MaterialDragCallback.kt | 1 + .../auxio/list/recycler/ViewHolders.kt | 8 +++++- .../list/selection/SelectionViewModel.kt | 9 +++++- .../oxycblt/auxio/music/MusicRepository.kt | 14 ++++++++-- .../auxio/music/cache/CacheRepository.kt | 2 +- .../auxio/music/device/DeviceLibrary.kt | 8 +++++- .../auxio/music/device/DeviceMusicImpl.kt | 12 ++++++-- .../oxycblt/auxio/music/device/RawMusic.kt | 4 ++- .../oxycblt/auxio/music/info/ReleaseType.kt | 1 + .../auxio/music/system/IndexerService.kt | 13 ++++++--- .../oxycblt/auxio/music/user/PlaylistImpl.kt | 6 +++- .../oxycblt/auxio/music/user/RawPlaylist.kt | 7 ++++- .../oxycblt/auxio/music/user/UserLibrary.kt | 6 +++- .../auxio/music/user/UserMusicDatabase.kt | 9 +++++- .../picker/NavigationPickerViewModel.kt | 6 +++- .../auxio/playback/PlaybackBarFragment.kt | 3 +- .../playback/PlaybackBottomSheetBehavior.kt | 3 +- .../auxio/playback/PlaybackViewModel.kt | 14 ++++++++-- .../picker/PlaybackPickerViewModel.kt | 5 +++- .../org/oxycblt/auxio/playback/queue/Queue.kt | 2 ++ .../auxio/playback/queue/QueueAdapter.kt | 14 +++++++--- .../queue/QueueBottomSheetBehavior.kt | 3 +- .../playback/state/PlaybackStateManager.kt | 1 + .../auxio/playback/system/PlaybackService.kt | 6 +++- .../org/oxycblt/auxio/search/SearchAdapter.kt | 20 +++++++++++-- .../oxycblt/auxio/search/SearchFragment.kt | 17 +++++++++-- .../oxycblt/auxio/search/SearchViewModel.kt | 4 ++- .../ui/PreferenceHeaderItemDecoration.kt | 2 +- .../auxio/ui/RippleFixMaterialButton.kt | 2 +- .../oxycblt/auxio/ui/accent/AccentAdapter.kt | 7 +++-- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 1 - .../oxycblt/auxio/widgets/WidgetProvider.kt | 4 ++- .../auxio/music/device/DeviceMusicImplTest.kt | 2 +- .../auxio/music/device/FakeDeviceLibrary.kt | 6 +++- gradle.properties | 4 +-- 66 files changed, 359 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java index a472ce7ab..e8c26ff3b 100644 --- a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java +++ b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java @@ -16,8 +16,6 @@ package com.google.android.material.bottomsheet; -import com.google.android.material.R; - import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static java.lang.Math.max; import static java.lang.Math.min; @@ -44,6 +42,7 @@ import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; + import androidx.annotation.FloatRange; import androidx.annotation.IntDef; import androidx.annotation.NonNull; @@ -63,11 +62,14 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit import androidx.core.view.accessibility.AccessibilityViewCommand; import androidx.customview.view.AbsSavedState; import androidx.customview.widget.ViewDragHelper; + +import com.google.android.material.R; import com.google.android.material.internal.ViewUtils; import com.google.android.material.internal.ViewUtils.RelativePadding; import com.google.android.material.resources.MaterialResources; import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.shape.ShapeAppearanceModel; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; diff --git a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java b/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java index 066429513..26de5108b 100644 --- a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java +++ b/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java @@ -16,20 +16,16 @@ package com.google.android.material.divider; -import com.google.android.material.R; - import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerView.ItemDecoration; import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; + import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.DimenRes; @@ -39,6 +35,11 @@ import androidx.annotation.Px; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ItemDecoration; + +import com.google.android.material.R; import com.google.android.material.internal.ThemeEnforcement; import com.google.android.material.resources.MaterialResources; diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 665fc7bdc..e3abffd85 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -31,6 +31,7 @@ import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController +import com.google.android.material.R as MR import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.transition.MaterialFadeThrough @@ -50,7 +51,15 @@ import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior import org.oxycblt.auxio.ui.ViewBindingFragment -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.collect +import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.coordinatorLayoutBehavior +import org.oxycblt.auxio.util.getAttrColorCompat +import org.oxycblt.auxio.util.getDimen +import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.systemBarInsetsCompat +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A wrapper around the home fragment that shows the playback fragment and controls the more @@ -122,7 +131,7 @@ class MainFragment : // Emulate the elevated bottom sheet style. background = MaterialShapeDrawable.createWithElevationOverlay(context).apply { - fillColor = context.getAttrColorCompat(R.attr.colorSurface) + fillColor = context.getAttrColorCompat(MR.attr.colorSurface) elevation = context.getDimen(R.dimen.elevation_normal) } // Apply bar insets for the queue's RecyclerView to usee. 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 d1f13160c..c6d534634 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -51,7 +51,14 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.canScroll +import org.oxycblt.auxio.util.collect +import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.setFullWidthLookup +import org.oxycblt.auxio.util.showToast +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [ListFragment] that shows information about an [Album]. 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 4677aee62..f1d699563 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -49,7 +49,13 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.collect +import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.setFullWidthLookup +import org.oxycblt.auxio.util.showToast +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [ListFragment] that shows information about an [Artist]. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 92052a955..0e011109c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -37,12 +37,22 @@ import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.UpdateInstructions -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.metadata.AudioProperties import org.oxycblt.auxio.playback.PlaybackSettings -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.Event +import org.oxycblt.auxio.util.MutableEvent +import org.oxycblt.auxio.util.logD /** * [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the @@ -60,7 +70,7 @@ constructor( private val playbackSettings: PlaybackSettings ) : ViewModel(), MusicRepository.UpdateListener { // --- SONG --- - + private var currentSongJob: Job? = null private val _currentSong = MutableStateFlow(null) 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 4ef67d581..387f16b9f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -41,10 +41,22 @@ import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.selection.SelectionViewModel -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.collect +import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.setFullWidthLookup +import org.oxycblt.auxio.util.showToast +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [ListFragment] that shows information for a particular [Genre]. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index c7cf92cde..10f0a8411 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -44,10 +44,22 @@ import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.selection.SelectionViewModel -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.collect +import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.setFullWidthLookup +import org.oxycblt.auxio.util.showToast +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [ListFragment] that shows information for a particular [Playlist]. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt index f03ad5c31..716d70c60 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt @@ -22,8 +22,8 @@ import android.content.Context import android.os.Build import android.util.AttributeSet import android.view.View +import androidx.appcompat.R import com.google.android.material.textfield.TextInputEditText -import org.oxycblt.auxio.R /** * A [TextInputEditText] that deliberately restricts all input except for selection. This will work diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt index e281d9982..ea3febed1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt @@ -29,7 +29,10 @@ import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt index 9c43dc875..c89eabf87 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt @@ -26,13 +26,16 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.databinding.ItemSortHeaderBinding +import org.oxycblt.auxio.detail.list.DetailListAdapter.Listener import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener -import org.oxycblt.auxio.list.adapter.* -import org.oxycblt.auxio.list.recycler.* +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.adapter.SimpleDiffCallback +import org.oxycblt.auxio.list.recycler.BasicHeaderViewHolder +import org.oxycblt.auxio.list.recycler.DividerViewHolder import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index 7b4147621..b328394fb 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -27,6 +27,7 @@ import androidx.appcompat.widget.TooltipCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.R as MR import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R @@ -213,7 +214,7 @@ private constructor(private val binding: ItemEditableSongBinding) : override val delete = binding.background override val background = MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { - fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) + fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface) elevation = binding.context.getDimen(R.dimen.elevation_normal) alpha = 0 } @@ -223,7 +224,7 @@ private constructor(private val binding: ItemEditableSongBinding) : LayerDrawable( arrayOf( MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply { - fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) + fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface) }, background)) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt index 690f3a792..29f4bf2d2 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt @@ -24,7 +24,8 @@ import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.ItemSongPropertyBinding import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.list.adapter.* +import org.oxycblt.auxio.list.adapter.FlexibleListAdapter +import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater diff --git a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt index 2b0cd3d5e..9a396aaa3 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt @@ -22,8 +22,8 @@ import android.content.Context import android.util.AttributeSet import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import com.google.android.material.R import com.google.android.material.floatingactionbutton.FloatingActionButton -import org.oxycblt.auxio.R import org.oxycblt.auxio.util.logD /** diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 5f26f32d5..862028000 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -46,16 +46,38 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeBinding -import org.oxycblt.auxio.home.list.* +import org.oxycblt.auxio.home.list.AlbumListFragment +import org.oxycblt.auxio.home.list.ArtistListFragment +import org.oxycblt.auxio.home.list.GenreListFragment +import org.oxycblt.auxio.home.list.PlaylistListFragment +import org.oxycblt.auxio.home.list.SongListFragment import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.list.selection.SelectionViewModel -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.IndexingProgress +import org.oxycblt.auxio.music.IndexingState +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.NoAudioPermissionException +import org.oxycblt.auxio.music.NoMusicException +import org.oxycblt.auxio.music.PERMISSION_READ_AUDIO +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.MainNavigationAction import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.collect +import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.getColorCompat +import org.oxycblt.auxio.util.lazyReflectedField +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.unlikelyToBeNull /** * The starting [SelectionFragment] of Auxio. Shows the user's music library and enables navigation diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 8b4e6d581..9003af558 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -26,7 +26,14 @@ import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.UpdateInstructions -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt index 3a848edf9..620ac018f 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt @@ -33,6 +33,7 @@ import android.text.TextUtils import android.util.AttributeSet import android.view.Gravity import androidx.core.widget.TextViewCompat +import com.google.android.material.R as MR import com.google.android.material.textview.MaterialTextView import org.oxycblt.auxio.R import org.oxycblt.auxio.util.getAttrColorCompat @@ -53,7 +54,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) minimumHeight = context.getDimenPixels(R.dimen.fast_scroll_popup_min_height) TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge) - setTextColor(context.getAttrColorCompat(R.attr.colorOnSecondary)) + setTextColor(context.getAttrColorCompat(MR.attr.colorOnSecondary)) ellipsize = TextUtils.TruncateAt.MIDDLE gravity = Gravity.CENTER includeFontPadding = false @@ -67,7 +68,10 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) private val paint: Paint = Paint().apply { isAntiAlias = true - color = context.getAttrColorCompat(R.attr.colorSecondary).defaultColor + color = + context + .getAttrColorCompat(com.google.android.material.R.attr.colorSecondary) + .defaultColor style = Paint.Style.FILL } diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index 1d0cc6737..991fc6f4e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -37,7 +37,12 @@ import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.list.recycler.AuxioRecyclerView -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.getDimenPixels +import org.oxycblt.auxio.util.getDrawableCompat +import org.oxycblt.auxio.util.getInteger +import org.oxycblt.auxio.util.isRtl +import org.oxycblt.auxio.util.isUnder +import org.oxycblt.auxio.util.systemBarInsetsCompat /** * A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of 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 a17172d08..c81b15ee1 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 @@ -30,13 +30,17 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding 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.SelectableListListener import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.AlbumViewHolder import org.oxycblt.auxio.list.selection.SelectionViewModel -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs 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 7eb5c88a0..7fd4adf23 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 @@ -28,8 +28,8 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding 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.SelectableListListener import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.ArtistViewHolder 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 8b2cab6f3..d1f44eb0d 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 @@ -28,8 +28,8 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding 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.SelectableListListener import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.GenreViewHolder diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt index 4c5d8d19a..1499fcb8f 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt @@ -27,8 +27,8 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding 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.SelectableListListener import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.PlaylistViewHolder 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 a21a470df..12423dabf 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,8 +30,8 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding 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.SelectableListListener import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SongViewHolder diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt index 449f489fc..a2a58abef 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -28,9 +28,14 @@ import android.widget.FrameLayout import android.widget.ImageView import androidx.annotation.AttrRes import androidx.core.view.updateMarginsRelative +import com.google.android.material.R as MR import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getDimenPixels @@ -80,7 +85,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr PlaybackIndicatorView(context).apply { cornerRadius = this@ImageGroup.cornerRadius } selectionIndicatorView = ImageView(context).apply { - imageTintList = context.getAttrColorCompat(R.attr.colorOnPrimary) + imageTintList = context.getAttrColorCompat(MR.attr.colorOnPrimary) setImageResource(R.drawable.ic_check_20) setBackgroundResource(R.drawable.ui_selection_badge_bg) } diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt index b3bb1cf2f..a45fc4cbb 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt @@ -22,7 +22,6 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.oxycblt.auxio.image.extractor.* @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index 2e04617e5..3f732f352 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -38,7 +38,12 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.image.extractor.SquareFrameTransform -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getDrawableCompat diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index 4e8e6d6d6..44059c2b5 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -24,7 +24,7 @@ import coil.key.Keyer import coil.request.Options import coil.size.Size import javax.inject.Inject -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Song class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) : Keyer> { diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 49655d01b..4be7af179 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -28,7 +28,12 @@ import androidx.viewbinding.ViewBinding import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R import org.oxycblt.auxio.list.selection.SelectionFragment -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.MainNavigationAction import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.util.logD diff --git a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt index 5002e60cf..06f0e60e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt @@ -22,8 +22,15 @@ import androidx.annotation.IdRes import kotlin.math.max import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R +import org.oxycblt.auxio.list.Sort.Direction import org.oxycblt.auxio.list.Sort.Mode -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Disc diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt index b9d77b0f8..3bdf330d9 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt @@ -20,7 +20,8 @@ package org.oxycblt.auxio.list.adapter import android.os.Handler import android.os.Looper -import androidx.recyclerview.widget.* +import androidx.recyclerview.widget.AdapterListUpdateCallback +import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import java.util.concurrent.Executor diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt index 96ef1ffcd..9fc255ce1 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt @@ -29,6 +29,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.divider.MaterialDivider import org.oxycblt.auxio.R +import org.oxycblt.auxio.list.recycler.DialogRecyclerView.ViewHolder import org.oxycblt.auxio.util.getDimenPixels /** diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt index ea5629e78..0143fe15b 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt @@ -26,6 +26,7 @@ import androidx.core.view.isInvisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R +import org.oxycblt.auxio.list.recycler.MaterialDragCallback.ViewHolder import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.logD diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index 0c9962996..6c575445c 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -31,7 +31,13 @@ import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.areNamesTheSame +import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.inflater diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index 5329151ad..ef3f54f60 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -23,7 +23,14 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song /** * A [ViewModel] that manages the current selection. diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 6fa0b4f79..56014b7f9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -21,12 +21,22 @@ package org.oxycblt.auxio.music import android.content.Context import android.content.pm.PackageManager import androidx.core.content.ContextCompat -import java.util.* +import java.util.LinkedList import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield +import org.oxycblt.auxio.music.MusicRepository.IndexingListener +import org.oxycblt.auxio.music.MusicRepository.IndexingWorker +import org.oxycblt.auxio.music.MusicRepository.UpdateListener import org.oxycblt.auxio.music.cache.CacheRepository import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.RawSong diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt index 967d53d68..1e227e53c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt @@ -20,7 +20,7 @@ package org.oxycblt.auxio.music.cache import javax.inject.Inject import org.oxycblt.auxio.music.device.RawSong -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.logE /** * A repository allowing access to cached metadata obtained in prior music loading operations. diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index af42d85ab..a4631a9ea 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -23,7 +23,13 @@ import android.net.Uri import android.provider.OpenableColumns import javax.inject.Inject import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.music.fs.useQuery import org.oxycblt.auxio.util.logD diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index c98049d68..74ab423bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -20,13 +20,21 @@ package org.oxycblt.auxio.music.device import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.toAudioUri import org.oxycblt.auxio.music.fs.toCoverUri -import org.oxycblt.auxio.music.info.* import org.oxycblt.auxio.music.info.Date +import org.oxycblt.auxio.music.info.Disc +import org.oxycblt.auxio.music.info.Name +import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.metadata.parseId3GenreNames import org.oxycblt.auxio.music.metadata.parseMultiValue import org.oxycblt.auxio.util.nonZeroOrNull diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 23b02b2f1..10d248991 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -19,7 +19,9 @@ package org.oxycblt.auxio.music.device import java.util.UUID -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.Directory import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.ReleaseType diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt b/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt index 20ac60034..c4cb00fd3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.music.info import org.oxycblt.auxio.R +import org.oxycblt.auxio.music.info.ReleaseType.Album /** * The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc. diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index eee390b11..4588b9915 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -28,12 +28,17 @@ import android.os.PowerManager import android.provider.MediaStore import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint -import java.lang.Runnable -import java.util.* import javax.inject.Inject -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.oxycblt.auxio.BuildConfig -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.IndexingProgress +import org.oxycblt.auxio.music.IndexingState +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.service.ForegroundManager diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index 00127a846..1194ea028 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -18,7 +18,11 @@ package org.oxycblt.auxio.music.user -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.info.Name diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt b/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt index 96d3b5e77..27cf62554 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt @@ -18,7 +18,12 @@ package org.oxycblt.auxio.music.user -import androidx.room.* +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.Junction +import androidx.room.PrimaryKey +import androidx.room.Relation import org.oxycblt.auxio.music.Music /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index fc64f5918..2b8bf962d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -20,7 +20,11 @@ package org.oxycblt.auxio.music.user import javax.inject.Inject import kotlinx.coroutines.channels.Channel -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.device.DeviceLibrary /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt index 087e46b35..9c46bbe78 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt @@ -18,7 +18,14 @@ package org.oxycblt.auxio.music.user -import androidx.room.* +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.RoomDatabase +import androidx.room.Transaction +import androidx.room.TypeConverters import org.oxycblt.auxio.music.Music /** diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt index b09b74ae9..f7f011ca3 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt @@ -23,7 +23,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.Song /** * A [ViewModel] that stores the current information required for navigation picker dialogs diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index f49d6df44..e5be7d6ca 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -21,6 +21,7 @@ package org.oxycblt.auxio.playback import android.os.Bundle import android.view.LayoutInflater import androidx.fragment.app.activityViewModels +import com.google.android.material.R as MR import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding @@ -95,7 +96,7 @@ class PlaybackBarFragment : ViewBindingFragment() { binding.playbackSecondaryAction.apply { setIconResource(R.drawable.ic_skip_next_24) contentDescription = getString(R.string.desc_skip_next) - iconTint = context.getAttrColorCompat(R.attr.colorOnSurfaceVariant) + iconTint = context.getAttrColorCompat(MR.attr.colorOnSurfaceVariant) setOnClickListener { playbackModel.next() } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt index a1a7b86b9..a2ed51882 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt @@ -24,6 +24,7 @@ import android.util.AttributeSet import android.view.MotionEvent import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.R as MR import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.BaseBottomSheetBehavior @@ -39,7 +40,7 @@ class PlaybackBottomSheetBehavior(context: Context, attributeSet: Attr BaseBottomSheetBehavior(context, attributeSet) { val sheetBackgroundDrawable = MaterialShapeDrawable.createWithElevationOverlay(context).apply { - fillColor = context.getAttrColorCompat(R.attr.colorSurface) + fillColor = context.getAttrColorCompat(MR.attr.colorSurface) elevation = context.getDimen(R.dimen.elevation_normal) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 68f2cac16..89ca23c43 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -27,10 +27,20 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.queue.Queue -import org.oxycblt.auxio.playback.state.* +import org.oxycblt.auxio.playback.state.InternalPlayer +import org.oxycblt.auxio.playback.state.PlaybackStateManager +import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt index 577b93c50..313e1b4d4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt @@ -23,7 +23,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.Song /** * A [ViewModel] that stores the choices shown in the playback picker dialogs. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt index 434e8f479..3a54bc1c6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt @@ -23,6 +23,8 @@ import kotlin.random.nextInt import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.queue.Queue.Change.Type +import org.oxycblt.auxio.playback.queue.Queue.SavedState /** * A heap-backed play queue. 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 76625a038..c9425bb82 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 @@ -24,16 +24,22 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.R as MR import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemEditableSongBinding import org.oxycblt.auxio.list.EditClickListListener -import org.oxycblt.auxio.list.adapter.* +import org.oxycblt.auxio.list.adapter.FlexibleListAdapter +import org.oxycblt.auxio.list.adapter.PlayingIndicatorAdapter import org.oxycblt.auxio.list.recycler.MaterialDragCallback import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.getAttrColorCompat +import org.oxycblt.auxio.util.getDimen +import org.oxycblt.auxio.util.inflater +import org.oxycblt.auxio.util.logD /** * A [RecyclerView.Adapter] that shows an editable list of queue items. @@ -110,7 +116,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS override val delete = binding.background override val background = MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { - fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) + fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface) elevation = binding.context.getDimen(R.dimen.elevation_normal) * 5 alpha = 0 } @@ -128,7 +134,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS LayerDrawable( arrayOf( MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply { - fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) + fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface) elevation = binding.context.getDimen(R.dimen.elevation_normal) }, background)) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt index d2043f373..ddf70b00d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt @@ -23,6 +23,7 @@ import android.util.AttributeSet import android.view.View import android.view.WindowInsets import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.R as MR import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.BaseBottomSheetBehavior @@ -64,7 +65,7 @@ class QueueBottomSheetBehavior(context: Context, attributeSet: Attribu override fun createBackground(context: Context) = MaterialShapeDrawable.createWithElevationOverlay(context).apply { // The queue sheet's background is a static elevated background. - fillColor = context.getAttrColorCompat(R.attr.colorSurface) + fillColor = context.getAttrColorCompat(MR.attr.colorSurface) elevation = context.getDimen(R.dimen.elevation_normal) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 63fb85ed2..6562db8cf 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -24,6 +24,7 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.queue.EditableQueue import org.oxycblt.auxio.playback.queue.Queue +import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 18c948326..cfee787cc 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -26,7 +26,11 @@ import android.content.IntentFilter import android.media.AudioManager import android.media.audiofx.AudioEffect import android.os.IBinder -import androidx.media3.common.* +import androidx.media3.common.AudioAttributes +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.RenderersFactory 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 4c1b2c2a7..a800221fb 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -20,11 +20,25 @@ package org.oxycblt.auxio.search import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.list.* +import org.oxycblt.auxio.list.BasicHeader +import org.oxycblt.auxio.list.Divider +import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback -import org.oxycblt.auxio.list.recycler.* -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.list.recycler.AlbumViewHolder +import org.oxycblt.auxio.list.recycler.ArtistViewHolder +import org.oxycblt.auxio.list.recycler.BasicHeaderViewHolder +import org.oxycblt.auxio.list.recycler.DividerViewHolder +import org.oxycblt.auxio.list.recycler.GenreViewHolder +import org.oxycblt.auxio.list.recycler.PlaylistViewHolder +import org.oxycblt.auxio.list.recycler.SongViewHolder +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.util.logD /** 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 a7b29b204..a79371305 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -39,10 +39,23 @@ import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.selection.SelectionViewModel -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.collect +import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.getSystemServiceCompat +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.setFullWidthLookup /** * The [ListFragment] providing search functionality for the music library. diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index ec42ca3cb..db1cc3c09 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -33,7 +33,9 @@ import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.playback.PlaybackSettings diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt index 10fe7f13e..ff54bcfac 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt @@ -25,8 +25,8 @@ import androidx.preference.PreferenceCategory import androidx.preference.PreferenceGroupAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.R import com.google.android.material.divider.BackportMaterialDividerItemDecoration -import org.oxycblt.auxio.R /** * A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly diff --git a/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt index c1d1074a9..2586745ae 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt @@ -21,8 +21,8 @@ package org.oxycblt.auxio.ui import android.content.Context import android.util.AttributeSet import androidx.annotation.AttrRes +import com.google.android.material.R import com.google.android.material.button.MaterialButton -import org.oxycblt.auxio.R import org.oxycblt.auxio.util.fixDoubleRipple /** diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt index 09eb411ef..41138d718 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt @@ -18,11 +18,12 @@ package org.oxycblt.auxio.ui.accent +import android.R as SR import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.TooltipCompat import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.R +import com.google.android.material.R as MR import org.oxycblt.auxio.databinding.ItemAccentBinding import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.util.getAttrColorCompat @@ -118,9 +119,9 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin binding.accent.apply { iconTint = if (isSelected) { - context.getAttrColorCompat(R.attr.colorSurface) + context.getAttrColorCompat(MR.attr.colorSurface) } else { - context.getColorCompat(android.R.color.transparent) + context.getColorCompat(SR.color.transparent) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 71fc880d6..67d5d68ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -34,7 +34,6 @@ import androidx.navigation.NavDirections import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -import java.lang.IllegalArgumentException /** * Get if this [View] contains the given [PointF], with optional leeway. diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 88e3dffa0..ececdf6e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -34,7 +34,9 @@ import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.ui.UISettings -import org.oxycblt.auxio.util.* +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.newBroadcastPendingIntent /** * The [AppWidgetProvider] for the "Now Playing" widget. This widget shows the current playback diff --git a/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt b/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt index 9227b93bf..2c4805486 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/device/DeviceMusicImplTest.kt @@ -18,7 +18,7 @@ package org.oxycblt.auxio.music.device -import java.util.* +import java.util.UUID import org.junit.Assert.assertTrue import org.junit.Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt b/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt index 93cbaa62c..d08e04615 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt @@ -20,7 +20,11 @@ package org.oxycblt.auxio.music.device import android.content.Context import android.net.Uri -import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.Song open class FakeDeviceLibrary : DeviceLibrary { override val songs: List diff --git a/gradle.properties b/gradle.properties index 8e89fa623..87899875b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,5 +21,5 @@ android.enableJetifier=false kotlin.code.style=official android.enableR8.fullMode=true android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false -android.nonFinalResIds=false \ No newline at end of file +android.nonTransitiveRClass=true +android.nonFinalResIds=true \ No newline at end of file From d3c8304a0d2892fbe339bc2011879e07a9deff1b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 25 May 2023 19:31:36 +0300 Subject: [PATCH 05/43] Add missing imports --- .../main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt | 1 + .../main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt | 1 + .../main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt | 1 + .../main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt | 1 + 4 files changed, 4 insertions(+) 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 1813f5cd4..d5932974e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -57,6 +57,7 @@ import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup +import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull 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 cd25fde8c..4dca06a0e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -54,6 +54,7 @@ import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup +import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull 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 d302f41a8..b3d417761 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -55,6 +55,7 @@ import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup +import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 1ceb225ec..5b63806ad 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -58,6 +58,7 @@ import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup +import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull From 2d0a74122f2bc98d7bd1e36bfe2a093fd67b6909 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 25 May 2023 11:37:15 -0600 Subject: [PATCH 06/43] ui: clean up share impl Clean up the sharing implementation to be more in-line with how I would write it. --- CHANGELOG.md | 5 ++ .../org/oxycblt/auxio/util/FrameworkUtil.kt | 48 ++++++++----------- app/src/main/res/menu/menu_album_detail.xml | 6 +-- .../main/res/menu/menu_album_song_actions.xml | 6 +-- .../res/menu/menu_artist_song_actions.xml | 6 +-- app/src/main/res/menu/menu_parent_actions.xml | 6 +-- app/src/main/res/menu/menu_playback.xml | 6 +-- .../res/menu/menu_playlist_song_actions.xml | 3 ++ app/src/main/res/menu/menu_song_actions.xml | 6 +-- 9 files changed, 47 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63626d664..77b90dedf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## dev + +#### What's New +- Added ability to share a track + ## 3.1.0 #### What's New diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index b70c6051a..f3091603e 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -36,12 +36,10 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import java.lang.IllegalArgumentException -import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song -private const val MIME_TYPE_FALLBACK = "audio/*" - /** * Get if this [View] contains the given [PointF], with optional leeway. * @@ -271,35 +269,31 @@ fun WindowInsets.replaceSystemBarInsetsCompat( } /** - * Show system share sheet to share songs + * Share a single [Song]. * - * @param music the [Music] to share + * @param song */ -fun Context.share(music: Music) = share( - when (music) { - is Song -> listOf(music) - is MusicParent -> music.songs - } -) +fun Context.share(song: Song) = share(listOf(song)) /** - * Show system share sheet to share multiple song + * Share all songs in a [MusicParent]. * - * @param songs the collection of [Song] to share + * @param parent The [MusicParent] to share. */ -fun Context.share(songs: Collection) { - if (songs.isEmpty()) { - return +fun Context.share(parent: MusicParent) = share(parent.songs) + +/** + * Share an arbitrary list of [Song]s. + * @param songs The [Song]s to share. + */ +fun Context.share(songs: List) { + if (songs.isEmpty()) return + val builder = ShareCompat.IntentBuilder(this) + val mimeTypes = mutableSetOf() + for (song in songs) { + builder.addStream(song.uri) + mimeTypes.add(song.mimeType.fromFormat ?: song.mimeType.fromExtension) } - val type = songs.mapTo(HashSet(songs.size)) { - it.mimeType.raw - }.singleOrNull() ?: MIME_TYPE_FALLBACK - ShareCompat.IntentBuilder(this) - .apply { - for (song in songs) { - addStream(song.uri) - } - } - .setType(type) - .startChooser() + + builder.setType(mimeTypes.singleOrNull() ?: "audio/*").startChooser() } diff --git a/app/src/main/res/menu/menu_album_detail.xml b/app/src/main/res/menu/menu_album_detail.xml index f0b18e132..7cc2b4b79 100644 --- a/app/src/main/res/menu/menu_album_detail.xml +++ b/app/src/main/res/menu/menu_album_detail.xml @@ -9,10 +9,10 @@ - + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_album_song_actions.xml b/app/src/main/res/menu/menu_album_song_actions.xml index d356eeedb..7325144c0 100644 --- a/app/src/main/res/menu/menu_album_song_actions.xml +++ b/app/src/main/res/menu/menu_album_song_actions.xml @@ -12,10 +12,10 @@ - + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_artist_song_actions.xml b/app/src/main/res/menu/menu_artist_song_actions.xml index aaad4040a..78442df43 100644 --- a/app/src/main/res/menu/menu_artist_song_actions.xml +++ b/app/src/main/res/menu/menu_artist_song_actions.xml @@ -12,10 +12,10 @@ - + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_parent_actions.xml b/app/src/main/res/menu/menu_parent_actions.xml index b741fd51b..6de2527de 100644 --- a/app/src/main/res/menu/menu_parent_actions.xml +++ b/app/src/main/res/menu/menu_parent_actions.xml @@ -12,10 +12,10 @@ - + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_playback.xml b/app/src/main/res/menu/menu_playback.xml index 5dca5f5dd..92264d881 100644 --- a/app/src/main/res/menu/menu_playback.xml +++ b/app/src/main/res/menu/menu_playback.xml @@ -14,9 +14,6 @@ android:id="@+id/action_go_album" android:title="@string/lbl_go_album" app:showAsAction="never" /> - @@ -24,4 +21,7 @@ android:id="@+id/action_song_detail" android:title="@string/lbl_song_detail" app:showAsAction="never" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_playlist_song_actions.xml b/app/src/main/res/menu/menu_playlist_song_actions.xml index 28c508681..e55d8e3f6 100644 --- a/app/src/main/res/menu/menu_playlist_song_actions.xml +++ b/app/src/main/res/menu/menu_playlist_song_actions.xml @@ -15,4 +15,7 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_song_actions.xml b/app/src/main/res/menu/menu_song_actions.xml index cba78013b..b892ba7c3 100644 --- a/app/src/main/res/menu/menu_song_actions.xml +++ b/app/src/main/res/menu/menu_song_actions.xml @@ -15,10 +15,10 @@ - + \ No newline at end of file From 4210a8d2477c19f5d6607c797b3796e284994702 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 25 May 2023 11:55:49 -0600 Subject: [PATCH 07/43] ui: disable some options w/empty parents Disable most playback and playlisting operations when an artist or playlist is empty. --- .../auxio/detail/ArtistDetailFragment.kt | 11 +- .../auxio/detail/PlaylistDetailFragment.kt | 12 +- .../org/oxycblt/auxio/list/ListFragment.kt | 304 +++++++++--------- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 1 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 183 insertions(+), 147 deletions(-) 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 4dca06a0e..310d9bc35 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -237,7 +237,16 @@ class ArtistDetailFragment : findNavController().navigateUp() return } - requireBinding().detailNormalToolbar.title = artist.name.resolve(requireContext()) + requireBinding().detailNormalToolbar.apply { + title = artist.name.resolve(requireContext()) + + // Disable options that make no sense with an empty artist + val playable = artist.songs.isNotEmpty() + menu.findItem(R.id.action_play_next).isEnabled = playable + menu.findItem(R.id.action_queue_add).isEnabled = playable + menu.findItem(R.id.action_playlist_add).isEnabled = playable + menu.findItem(R.id.action_share).isEnabled = playable + } artistHeaderAdapter.setParent(artist) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 5b63806ad..32044be87 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -255,8 +255,16 @@ class PlaylistDetailFragment : return } val binding = requireBinding() - binding.detailNormalToolbar.title = playlist.name.resolve(requireContext()) - binding.detailEditToolbar.title = "Editing ${playlist.name.resolve(requireContext())}" + binding.detailNormalToolbar.apply { + title = playlist.name.resolve(requireContext()) + // Disable options that make no sense with an empty playlist + val playable = playlist.songs.isNotEmpty() + menu.findItem(R.id.action_play_next).isEnabled = playable + menu.findItem(R.id.action_queue_add).isEnabled = playable + menu.findItem(R.id.action_share).isEnabled = playable + } + binding.detailEditToolbar.title = + getString(R.string.fmt_editing, playlist.name.resolve(requireContext())) playlistHeaderAdapter.setParent(playlist) } diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 7127400f4..3f2610c3f 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.list -import android.view.MenuItem import android.view.View import androidx.annotation.MenuRes import androidx.appcompat.widget.PopupMenu @@ -89,36 +88,39 @@ abstract class ListFragment : protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) { logD("Launching new song menu: ${song.name}") - openMusicMenuImpl(anchor, menuRes) { - when (it.itemId) { - R.id.action_play_next -> { - playbackModel.playNext(song) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_queue_add -> { - playbackModel.addToQueue(song) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_go_artist -> { - navModel.exploreNavigateToParentArtist(song) - } - R.id.action_go_album -> { - navModel.exploreNavigateTo(song.album) - } - R.id.action_share -> { - requireContext().share(song) - } - R.id.action_playlist_add -> { - musicModel.addToPlaylist(song) - } - R.id.action_song_detail -> { - navModel.mainNavigateTo( - MainNavigationAction.Directions( - MainFragmentDirections.actionShowDetails(song.uid))) - } - else -> { - error("Unexpected menu item selected") + openMenu(anchor, menuRes) { + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_play_next -> { + playbackModel.playNext(song) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(song) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_go_artist -> { + navModel.exploreNavigateToParentArtist(song) + } + R.id.action_go_album -> { + navModel.exploreNavigateTo(song.album) + } + R.id.action_share -> { + requireContext().share(song) + } + R.id.action_playlist_add -> { + musicModel.addToPlaylist(song) + } + R.id.action_song_detail -> { + navModel.mainNavigateTo( + MainNavigationAction.Directions( + MainFragmentDirections.actionShowDetails(song.uid))) + } + else -> { + error("Unexpected menu item selected") + } } + true } } } @@ -134,34 +136,37 @@ abstract class ListFragment : protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) { logD("Launching new album menu: ${album.name}") - openMusicMenuImpl(anchor, menuRes) { - when (it.itemId) { - R.id.action_play -> { - playbackModel.play(album) - } - R.id.action_shuffle -> { - playbackModel.shuffle(album) - } - R.id.action_play_next -> { - playbackModel.playNext(album) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_queue_add -> { - playbackModel.addToQueue(album) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_go_artist -> { - navModel.exploreNavigateToParentArtist(album) - } - R.id.action_playlist_add -> { - musicModel.addToPlaylist(album) - } - R.id.action_share -> { - requireContext().share(album) - } - else -> { - error("Unexpected menu item selected") + openMenu(anchor, menuRes) { + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_play -> { + playbackModel.play(album) + } + R.id.action_shuffle -> { + playbackModel.shuffle(album) + } + R.id.action_play_next -> { + playbackModel.playNext(album) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(album) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_go_artist -> { + navModel.exploreNavigateToParentArtist(album) + } + R.id.action_playlist_add -> { + musicModel.addToPlaylist(album) + } + R.id.action_share -> { + requireContext().share(album) + } + else -> { + error("Unexpected menu item selected") + } } + true } } } @@ -177,31 +182,42 @@ abstract class ListFragment : protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) { logD("Launching new artist menu: ${artist.name}") - openMusicMenuImpl(anchor, menuRes) { - when (it.itemId) { - R.id.action_play -> { - playbackModel.play(artist) - } - R.id.action_shuffle -> { - playbackModel.shuffle(artist) - } - R.id.action_play_next -> { - playbackModel.playNext(artist) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_queue_add -> { - playbackModel.addToQueue(artist) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_playlist_add -> { - musicModel.addToPlaylist(artist) - } - R.id.action_share -> { - requireContext().share(artist) - } - else -> { - error("Unexpected menu item selected") + openMenu(anchor, menuRes) { + val playable = artist.songs.isNotEmpty() + menu.findItem(R.id.action_play).isEnabled = playable + menu.findItem(R.id.action_shuffle).isEnabled = playable + menu.findItem(R.id.action_play_next).isEnabled = playable + menu.findItem(R.id.action_queue_add).isEnabled = playable + menu.findItem(R.id.action_playlist_add).isEnabled = playable + menu.findItem(R.id.action_share).isEnabled = playable + + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_play -> { + playbackModel.play(artist) + } + R.id.action_shuffle -> { + playbackModel.shuffle(artist) + } + R.id.action_play_next -> { + playbackModel.playNext(artist) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(artist) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_playlist_add -> { + musicModel.addToPlaylist(artist) + } + R.id.action_share -> { + requireContext().share(artist) + } + else -> { + error("Unexpected menu item selected") + } } + true } } } @@ -217,31 +233,34 @@ abstract class ListFragment : protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) { logD("Launching new genre menu: ${genre.name}") - openMusicMenuImpl(anchor, menuRes) { - when (it.itemId) { - R.id.action_play -> { - playbackModel.play(genre) - } - R.id.action_shuffle -> { - playbackModel.shuffle(genre) - } - R.id.action_play_next -> { - playbackModel.playNext(genre) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_queue_add -> { - playbackModel.addToQueue(genre) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_playlist_add -> { - musicModel.addToPlaylist(genre) - } - R.id.action_share -> { - requireContext().share(genre) - } - else -> { - error("Unexpected menu item selected") + openMenu(anchor, menuRes) { + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_play -> { + playbackModel.play(genre) + } + R.id.action_shuffle -> { + playbackModel.shuffle(genre) + } + R.id.action_play_next -> { + playbackModel.playNext(genre) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(genre) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_playlist_add -> { + musicModel.addToPlaylist(genre) + } + R.id.action_share -> { + requireContext().share(genre) + } + else -> { + error("Unexpected menu item selected") + } } + true } } } @@ -257,46 +276,43 @@ abstract class ListFragment : protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, playlist: Playlist) { logD("Launching new playlist menu: ${playlist.name}") - openMusicMenuImpl(anchor, menuRes) { - when (it.itemId) { - R.id.action_play -> { - playbackModel.play(playlist) - } - R.id.action_shuffle -> { - playbackModel.shuffle(playlist) - } - R.id.action_play_next -> { - playbackModel.playNext(playlist) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_queue_add -> { - playbackModel.addToQueue(playlist) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_rename -> { - musicModel.renamePlaylist(playlist) - } - R.id.action_delete -> { - musicModel.deletePlaylist(playlist) - } - R.id.action_share -> { - requireContext().share(playlist) - } - else -> { - error("Unexpected menu item selected") - } - } - } - } - - private fun openMusicMenuImpl( - anchor: View, - @MenuRes menuRes: Int, - onMenuItemClick: (MenuItem) -> Unit - ) { openMenu(anchor, menuRes) { - setOnMenuItemClickListener { item -> - onMenuItemClick(item) + val playable = playlist.songs.isNotEmpty() + menu.findItem(R.id.action_play).isEnabled = playable + menu.findItem(R.id.action_shuffle).isEnabled = playable + menu.findItem(R.id.action_play_next).isEnabled = playable + menu.findItem(R.id.action_queue_add).isEnabled = playable + menu.findItem(R.id.action_share).isEnabled = playable + + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_play -> { + playbackModel.play(playlist) + } + R.id.action_shuffle -> { + playbackModel.shuffle(playlist) + } + R.id.action_play_next -> { + playbackModel.playNext(playlist) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(playlist) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_rename -> { + musicModel.renamePlaylist(playlist) + } + R.id.action_delete -> { + musicModel.deletePlaylist(playlist) + } + R.id.action_share -> { + requireContext().share(playlist) + } + else -> { + error("Unexpected menu item selected") + } + } true } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index f3091603e..55939af17 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -284,6 +284,7 @@ fun Context.share(parent: MusicParent) = share(parent.songs) /** * Share an arbitrary list of [Song]s. + * * @param songs The [Song]s to share. */ fun Context.share(songs: List) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c09fbe4ea..e57eeb348 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -386,6 +386,8 @@ %d Selected + + Editing %s Disc %d From 8939d341e6b9c741d2be672ec5e94d0a27cc0cc4 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 25 May 2023 12:01:41 -0600 Subject: [PATCH 08/43] detail: default to "no disc" instead of "disc 1" Default tracks without a disc to a group called "No disc" instead of disc 1. This should reduce confusion on the user end, as it will make improper taggings more apparent instead of simply degrading to a werid sort ordering. Resolves #405. --- .../oxycblt/auxio/detail/DetailViewModel.kt | 7 ++-- .../detail/list/AlbumDetailListAdapter.kt | 40 ++++++++++++------- app/src/main/res/values/strings.xml | 1 + 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 0e011109c..26c64b2b1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.yield import org.oxycblt.auxio.R +import org.oxycblt.auxio.detail.list.DiscHeader import org.oxycblt.auxio.detail.list.EditHeader import org.oxycblt.auxio.detail.list.SortHeader import org.oxycblt.auxio.list.BasicHeader @@ -46,7 +47,6 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.metadata.AudioProperties import org.oxycblt.auxio.playback.PlaybackSettings @@ -409,12 +409,11 @@ constructor( // To create a good user experience regarding disc numbers, we group the album's // songs up by disc and then delimit the groups by a disc header. val songs = albumSongSort.songs(album.songs) - // Songs without disc tags become part of Disc 1. - val byDisc = songs.groupBy { it.disc ?: Disc(1, null) } + val byDisc = songs.groupBy { it.disc } if (byDisc.size > 1) { logD("Album has more than one disc, interspersing headers") for (entry in byDisc.entries) { - list.add(entry.key) + list.add(DiscHeader(entry.key)) list.addAll(entry.value) } } else { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt index b7217a681..15ece1b35 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt @@ -49,14 +49,14 @@ class AlbumDetailListAdapter(private val listener: Listener) : override fun getItemViewType(position: Int) = when (getItem(position)) { // Support sub-headers for each disc, and special album songs. - is Disc -> DiscViewHolder.VIEW_TYPE + is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE is Song -> AlbumSongViewHolder.VIEW_TYPE else -> super.getItemViewType(position) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - DiscViewHolder.VIEW_TYPE -> DiscViewHolder.from(parent) + DiscHeaderViewHolder.VIEW_TYPE -> DiscHeaderViewHolder.from(parent) AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent) else -> super.onCreateViewHolder(parent, viewType) } @@ -64,7 +64,7 @@ class AlbumDetailListAdapter(private val listener: Listener) : override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { super.onBindViewHolder(holder, position) when (val item = getItem(position)) { - is Disc -> (holder as DiscViewHolder).bind(item) + is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item) is Song -> (holder as AlbumSongViewHolder).bind(item, listener) } } @@ -76,7 +76,7 @@ class AlbumDetailListAdapter(private val listener: Listener) : override fun areContentsTheSame(oldItem: Item, newItem: Item) = when { oldItem is Disc && newItem is Disc -> - DiscViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + DiscHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is Song && newItem is Song -> AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) @@ -88,23 +88,35 @@ class AlbumDetailListAdapter(private val listener: Listener) : } /** - * A [RecyclerView.ViewHolder] that displays a [Disc] to delimit different disc groups. Use [from] - * to create an instance. + * A wrapper around [Disc] signifying that a header should be shown for a disc group. + * @author Alexander Capehart (OxygenCobalt) + */ +data class DiscHeader(val inner: Disc?) : Item + +/** + * A [RecyclerView.ViewHolder] that displays a [DiscHeader] to delimit different disc groups. Use + * [from] to create an instance. * * @author Alexander Capehart (OxygenCobalt) */ -private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) : +private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : RecyclerView.ViewHolder(binding.root) { /** * Bind new data to this instance. * - * @param disc The new [disc] to bind. + * @param discHeader The new [DiscHeader] to bind. */ - fun bind(disc: Disc) { - binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number) - binding.discName.apply { - text = disc.name - isGone = disc.name == null + fun bind(discHeader: DiscHeader) { + val disc = discHeader.inner + if (disc != null) { + binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number) + binding.discName.apply { + text = disc.name + isGone = text == null + } + } else { + binding.discNumber.text = binding.context.getString(R.string.def_disc) + binding.discName.isGone = true } } @@ -119,7 +131,7 @@ private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) : * @return A new instance. */ fun from(parent: View) = - DiscViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater)) + DiscHeaderViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e57eeb348..d0ab06f20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -332,6 +332,7 @@ Unknown artist Unknown genre No date + No disc No track No songs No music playing From ba94d4fa21235678800e370aad088fea98379ea4 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 25 May 2023 12:54:23 -0600 Subject: [PATCH 09/43] detail: group implicit albums in "appears on" Group albums implicitly linked to an artist via the "artist" tag into their own section called "Appears on". This makes Auxio's artist model a bit more apparent to users. Resolves #411. --- CHANGELOG.md | 9 +++ .../oxycblt/auxio/detail/DetailViewModel.kt | 57 +++++++++++-------- .../detail/list/AlbumDetailListAdapter.kt | 1 + .../org/oxycblt/auxio/home/HomeViewModel.kt | 2 +- .../java/org/oxycblt/auxio/music/Music.kt | 18 +++--- .../auxio/music/device/DeviceMusicImpl.kt | 19 ++++--- app/src/main/res/values/strings.xml | 6 +- 7 files changed, 69 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77b90dedf..9c3473dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ #### What's New - Added ability to share a track +#### What's Improved +- Tracks with no disc number now default to "No Disc" instead of "Disc 1" +- Albums implicitly linked only via "artist" tags are now placed in a special +"appears on" section in the artist view + +#### What's Fixed +- Prevented options such as "Add to queue" from being selected on empty +artists and playlists + ## 3.1.0 #### What's New diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 26c64b2b1..67ebf89f1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -428,32 +428,42 @@ constructor( private fun refreshArtistList(artist: Artist, replace: Boolean = false) { logD("Refreshing artist list") val list = mutableListOf() - val albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums) - val byReleaseGroup = - albums.groupBy { - // Remap the complicated ReleaseType data structure into an easier - // "AlbumGrouping" enum that will automatically group and sort - // the artist's albums. - when (it.releaseType.refinement) { - ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE - ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES - null -> - when (it.releaseType) { - is ReleaseType.Album -> AlbumGrouping.ALBUMS - is ReleaseType.EP -> AlbumGrouping.EPS - is ReleaseType.Single -> AlbumGrouping.SINGLES - is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS - is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS - is ReleaseType.Mix -> AlbumGrouping.MIXES - is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES - } + val grouping = + Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING) + .albums(artist.explicitAlbums) + .groupByTo(sortedMapOf()) { + // Remap the complicated ReleaseType data structure into an easier + // "AlbumGrouping" enum that will automatically group and sort + // the artist's albums. + when (it.releaseType.refinement) { + ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE + ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES + null -> + when (it.releaseType) { + is ReleaseType.Album -> AlbumGrouping.ALBUMS + is ReleaseType.EP -> AlbumGrouping.EPS + is ReleaseType.Single -> AlbumGrouping.SINGLES + is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS + is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS + is ReleaseType.Mix -> AlbumGrouping.DJMIXES + is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES + } + } } - } - logD("Release groups for this artist: ${byReleaseGroup.keys}") + if (artist.implicitAlbums.isNotEmpty()) { + // groupByTo normally returns a mapping to a MutableList mapping. Since MutableList + // inherits list, we can cast upwards and save a copy by directly inserting the + // implicit album list into the mapping. + @Suppress("UNCHECKED_CAST") + (grouping as MutableMap>)[AlbumGrouping.APPEARANCES] = + Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.implicitAlbums) + } - for (entry in byReleaseGroup.entries.sortedBy { it.key }) { + logD("Release groups for this artist: ${grouping.keys}") + + for (entry in grouping.entries) { val header = BasicHeader(entry.key.headerTitleRes) list.add(Divider(header)) list.add(header) @@ -533,8 +543,9 @@ constructor( SINGLES(R.string.lbl_singles), COMPILATIONS(R.string.lbl_compilations), SOUNDTRACKS(R.string.lbl_soundtracks), - MIXES(R.string.lbl_mixes), + DJMIXES(R.string.lbl_mixes), MIXTAPES(R.string.lbl_mixtapes), + APPEARANCES(R.string.lbl_appears_on), LIVE(R.string.lbl_live_group), REMIXES(R.string.lbl_remix_group), } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt index 15ece1b35..bb6dcf786 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt @@ -89,6 +89,7 @@ class AlbumDetailListAdapter(private val listener: Listener) : /** * A wrapper around [Disc] signifying that a header should be shown for a disc group. + * * @author Alexander Capehart (OxygenCobalt) */ data class DiscHeader(val inner: Disc?) : Item diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 9003af558..a584ace36 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -158,7 +158,7 @@ constructor( musicSettings.artistSort.artists( if (homeSettings.shouldHideCollaborators) { // Hide Collaborators is enabled, filter out collaborators. - deviceLibrary.artists.filter { !it.isCollaborator } + deviceLibrary.artists.filter { it.explicitAlbums.isNotEmpty() } } else { deviceLibrary.artists }) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index bcf2fb53e..b2e4553dc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -314,21 +314,23 @@ interface Album : MusicParent { */ interface Artist : MusicParent { /** - * All of the [Album]s this artist is credited to. Note that any [Song] credited to this artist - * will have it's [Album] considered to be "indirectly" linked to this [Artist], and thus - * included in this list. + * All of the [Album]s this artist is credited to from [explicitAlbums] and [implicitAlbums]. + * Note that any [Song] credited to this artist will have it's [Album] considered to be + * "indirectly" linked to this [Artist], and thus included in this list. */ val albums: List + + /** Albums directly credited to this [Artist] via a "Album Artist" tag. */ + val explicitAlbums: List + + /** Albums indirectly credited to this [Artist] via an "Artist" tag. */ + val implicitAlbums: List + /** * The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no * songs. */ val durationMs: Long? - /** - * Whether this artist is considered a "collaborator", i.e it is not directly credited on any - * [Album]. - */ - val isCollaborator: Boolean /** The [Genre]s of this artist. */ val genres: List } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 74ab423bf..204acad02 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -344,8 +344,9 @@ class ArtistImpl( override val songs: List override val albums: List + override val explicitAlbums: List + override val implicitAlbums: List override val durationMs: Long? - override val isCollaborator: Boolean // Note: Append song contents to MusicParent equality so that artists with // the same UID but different songs are not equal. @@ -366,30 +367,30 @@ class ArtistImpl( init { val distinctSongs = mutableSetOf() - val distinctAlbums = mutableSetOf() - - var noAlbums = true + val albumMap = mutableMapOf() for (music in songAlbums) { when (music) { is SongImpl -> { music.link(this) distinctSongs.add(music) - distinctAlbums.add(music.album) + if (albumMap[music.album] == null) { + albumMap[music.album] = false + } } is AlbumImpl -> { music.link(this) - distinctAlbums.add(music) - noAlbums = false + albumMap[music] = true } else -> error("Unexpected input music ${music::class.simpleName}") } } songs = distinctSongs.toList() - albums = distinctAlbums.toList() + albums = albumMap.keys.toList() + explicitAlbums = albumMap.entries.filter { it.value }.map { it.key } + implicitAlbums = albumMap.entries.filterNot { it.value }.map { it.key } durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull() - isCollaborator = noAlbums } /** diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d0ab06f20..139cc91c9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,14 +61,16 @@ Mixtape - Mixes + DJ Mixes - Mix + DJ Mix Live Remixes + + Appears on Artist Artists From 21a6b97bfa3646ff6aa4549773151e41d8f57500 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 25 May 2023 12:55:42 -0600 Subject: [PATCH 10/43] build: update deps AGP -> 8.0.2 Coil -> 2.4.0 --- app/build.gradle | 2 +- app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt | 6 ++++-- build.gradle | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4f968216b..aebcf367f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -125,7 +125,7 @@ dependencies { implementation project(":media-lib-decoder-ffmpeg") // Image loading - implementation 'io.coil-kt:coil-base:2.3.0' + implementation 'io.coil-kt:coil-base:2.4.0' // Material // TODO: Stuck on 1.8.0-alpha01 until ripple bug with tab layout is actually available diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt index a3b3b4bac..57aae6970 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt @@ -83,9 +83,11 @@ open class FakeArtist : Artist { get() = throw NotImplementedError() override val albums: List get() = throw NotImplementedError() - override val genres: List + override val explicitAlbums: List get() = throw NotImplementedError() - override val isCollaborator: Boolean + override val implicitAlbums: List + get() = throw NotImplementedError() + override val genres: List get() = throw NotImplementedError() override val durationMs: Long get() = throw NotImplementedError() diff --git a/build.gradle b/build.gradle index 754d9b9a6..dbb61d537 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.0.1' + classpath 'com.android.tools.build:gradle:8.0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version" classpath "com.diffplug.spotless:spotless-plugin-gradle:6.18.0" From c2def19aee79857ffe6371c904ce6dfd37468229 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 25 May 2023 13:10:49 -0600 Subject: [PATCH 11/43] ui: handle playing indicator edge cases Handle two edge cases identified with the playing indicator behavior: 1. When enqueing songs from another parent, the prior parent is still indicates as "playing" when it kind-of isn't. 2. When playback is stopped, the parent is not reset, and thus will still be indicated as "playing" after the song has disappeared. This is rarer and should be resolved in other ways, but the solution to 1 also fixes this. Resolves #380. --- .../auxio/detail/ArtistDetailFragment.kt | 6 +++--- .../auxio/detail/GenreDetailFragment.kt | 21 +++++++++++-------- .../auxio/home/list/AlbumListFragment.kt | 12 +++++++---- .../auxio/home/list/ArtistListFragment.kt | 12 +++++++---- .../auxio/home/list/GenreListFragment.kt | 12 +++++++---- .../auxio/home/list/PlaylistListFragment.kt | 14 +++++++------ .../auxio/home/list/SongListFragment.kt | 8 ++----- 7 files changed, 49 insertions(+), 36 deletions(-) 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 310d9bc35..5384ead4c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -254,14 +254,14 @@ class ArtistDetailFragment : val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value) val playingItem = when (parent) { - // Always highlight a playing album if it's from this artist. - is Album -> parent + // Always highlight a playing album if it's from this artist, and if the currently + // playing song is contained within. + is Album -> parent.takeIf { song?.album == it } // If the parent is the artist itself, use the currently playing song. currentArtist -> song // Nothing is playing from this artist. else -> null } - artistListAdapter.setPlaying(playingItem, isPlaying) } 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 b3d417761..c178f4a1a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -239,15 +239,18 @@ class GenreDetailFragment : } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - var playingMusic: Music? = null - if (parent is Artist) { - playingMusic = parent - } - // Prefer songs that might be playing from this genre. - if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) { - playingMusic = song - } - genreListAdapter.setPlaying(playingMusic, isPlaying) + val currentGenre = unlikelyToBeNull(detailModel.currentGenre.value) + val playingItem = + when (parent) { + // Always highlight a playing artist if it's from this genre, and if the currently + // playing song is contained within. + is Artist -> parent.takeIf { song?.run { artists.contains(it) } ?: false } + // If the parent is the artist itself, use the currently playing song. + currentGenre -> song + // Nothing is playing from this artist. + else -> null + } + genreListAdapter.setPlaying(playingItem, isPlaying) } private fun handleNavigation(item: Music?) { 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 c81b15ee1..31b6b67bf 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 @@ -41,6 +41,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs @@ -82,7 +83,8 @@ class AlbumListFragment : collectImmediately(homeModel.albumsList, ::updateAlbums) collectImmediately(selectionModel.selected, ::updateSelection) - collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) + collectImmediately( + playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } override fun onDestroyBinding(binding: FragmentHomeListBinding) { @@ -151,9 +153,11 @@ class AlbumListFragment : albumAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf())) } - private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { - // If an album is playing, highlight it within this adapter. - albumAdapter.setPlaying(parent as? Album, isPlaying) + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + // Only highlight the album if it is currently playing, and if the currently + // playing song is also contained within. + val playlist = (parent as? Album)?.takeIf { song?.album == it } + albumAdapter.setPlaying(playlist, isPlaying) } /** 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 7fd4adf23..46d42f16c 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 @@ -39,6 +39,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs @@ -78,7 +79,8 @@ class ArtistListFragment : collectImmediately(homeModel.artistsList, ::updateArtists) collectImmediately(selectionModel.selected, ::updateSelection) - collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) + collectImmediately( + playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } override fun onDestroyBinding(binding: FragmentHomeListBinding) { @@ -128,9 +130,11 @@ class ArtistListFragment : artistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf())) } - private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { - // If an artist is playing, highlight it within this adapter. - artistAdapter.setPlaying(parent as? Artist, isPlaying) + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + // Only highlight the artist if it is currently playing, and if the currently + // playing song is also contained within. + val playlist = (parent as? Artist)?.takeIf { song?.run { artists.contains(it) } ?: false } + artistAdapter.setPlaying(playlist, isPlaying) } /** 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 d1f44eb0d..d592ba377 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 @@ -39,6 +39,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs @@ -77,7 +78,8 @@ class GenreListFragment : collectImmediately(homeModel.genresList, ::updateGenres) collectImmediately(selectionModel.selected, ::updateSelection) - collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) + collectImmediately( + playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } override fun onDestroyBinding(binding: FragmentHomeListBinding) { @@ -127,9 +129,11 @@ class GenreListFragment : genreAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf())) } - private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { - // If a genre is playing, highlight it within this adapter. - genreAdapter.setPlaying(parent as? Genre, isPlaying) + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + // Only highlight the genre if it is currently playing, and if the currently + // playing song is also contained within. + val playlist = (parent as? Genre)?.takeIf { song?.run { genres.contains(it) } ?: false } + genreAdapter.setPlaying(playlist, isPlaying) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt index 1499fcb8f..167619c15 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt @@ -38,6 +38,7 @@ import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs @@ -48,8 +49,6 @@ import org.oxycblt.auxio.util.logD * A [ListFragment] that shows a list of [Playlist]s. * * @author Alexander Capehart (OxygenCobalt) - * - * TODO: Show a placeholder when there are no playlists. */ class PlaylistListFragment : ListFragment(), @@ -77,7 +76,8 @@ class PlaylistListFragment : collectImmediately(homeModel.playlistsList, ::updatePlaylists) collectImmediately(selectionModel.selected, ::updateSelection) - collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) + collectImmediately( + playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } override fun onDestroyBinding(binding: FragmentHomeListBinding) { @@ -128,9 +128,11 @@ class PlaylistListFragment : playlistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf())) } - private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { - // If a playlist is playing, highlight it within this adapter. - playlistAdapter.setPlaying(parent as? Playlist, isPlaying) + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + // Only highlight the playlist if it is currently playing, and if the currently + // playing song is also contained within. + val playlist = (parent as? Playlist)?.takeIf { it.songs.contains(song) } ?: return + playlistAdapter.setPlaying(playlist, isPlaying) } /** 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 12423dabf..62643f4cf 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 @@ -155,12 +155,8 @@ class SongListFragment : } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - if (parent == null) { - songAdapter.setPlaying(song, isPlaying) - } else { - // Ignore playback that is not from all songs - songAdapter.setPlaying(null, isPlaying) - } + // Only indicate playback that is from all songs + songAdapter.setPlaying(song.takeIf { parent == null }, isPlaying) } /** From b037cfb166771092d573ffd77ec12d9e010c51e9 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 25 May 2023 13:45:34 -0600 Subject: [PATCH 12/43] music: improve sorting Update sorting usage in-app so that it's only done when absolutely necessary. --- CHANGELOG.md | 6 ++- .../oxycblt/auxio/detail/DetailViewModel.kt | 40 +++++++++---------- .../header/ArtistDetailHeaderAdapter.kt | 2 + .../header/PlaylistDetailHeaderAdapter.kt | 1 - .../auxio/image/extractor/CoverExtractor.kt | 8 +++- .../java/org/oxycblt/auxio/music/Music.kt | 4 -- .../auxio/music/device/DeviceMusicImpl.kt | 11 ++--- .../oxycblt/auxio/music/user/PlaylistImpl.kt | 2 - 8 files changed, 35 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c3473dc0..e24e73339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ "appears on" section in the artist view #### What's Fixed -- Prevented options such as "Add to queue" from being selected on empty -artists and playlists +- Prevented options such as "Add to queue" from being selected on empty artists and playlists +- Fixed issue where an item would be indicated as "playing" after playback ended +- Items should no longer be indicated as playing if the currently playing song is not contained +within it ## 3.1.0 diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 67ebf89f1..0b1861f8e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -430,27 +430,25 @@ constructor( val list = mutableListOf() val grouping = - Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING) - .albums(artist.explicitAlbums) - .groupByTo(sortedMapOf()) { - // Remap the complicated ReleaseType data structure into an easier - // "AlbumGrouping" enum that will automatically group and sort - // the artist's albums. - when (it.releaseType.refinement) { - ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE - ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES - null -> - when (it.releaseType) { - is ReleaseType.Album -> AlbumGrouping.ALBUMS - is ReleaseType.EP -> AlbumGrouping.EPS - is ReleaseType.Single -> AlbumGrouping.SINGLES - is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS - is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS - is ReleaseType.Mix -> AlbumGrouping.DJMIXES - is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES - } - } + artist.explicitAlbums.groupByTo(sortedMapOf()) { + // Remap the complicated ReleaseType data structure into an easier + // "AlbumGrouping" enum that will automatically group and sort + // the artist's albums. + when (it.releaseType.refinement) { + ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE + ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES + null -> + when (it.releaseType) { + is ReleaseType.Album -> AlbumGrouping.ALBUMS + is ReleaseType.EP -> AlbumGrouping.EPS + is ReleaseType.Single -> AlbumGrouping.SINGLES + is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS + is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS + is ReleaseType.Mix -> AlbumGrouping.DJMIXES + is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES + } } + } if (artist.implicitAlbums.isNotEmpty()) { // groupByTo normally returns a mapping to a MutableList mapping. Since MutableList @@ -458,7 +456,7 @@ constructor( // implicit album list into the mapping. @Suppress("UNCHECKED_CAST") (grouping as MutableMap>)[AlbumGrouping.APPEARANCES] = - Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.implicitAlbums) + artist.implicitAlbums } logD("Release groups for this artist: ${grouping.keys}") diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt index 3b346975e..02303a566 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.inflater +import org.oxycblt.auxio.util.logD /** * A [DetailHeaderAdapter] that shows [Artist] information. @@ -91,6 +92,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) : // The artist does not have any songs, so hide functionality that makes no sense. // ex. Play and Shuffle, Song Counts, and Genre Information. // Artists are always guaranteed to have albums however, so continue to show those. + logD("Artist is empty, disabling genres and playback") binding.detailSubhead.isVisible = false binding.detailPlayButton.isEnabled = false binding.detailShuffleButton.isEnabled = false diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt index 08ce66ba4..375278a39 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt @@ -83,7 +83,6 @@ private constructor(private val binding: ItemDetailHeaderBinding) : editedPlaylist: List?, listener: DetailHeaderAdapter.Listener ) { - // TODO: Debug perpetually re-binding images binding.detailCover.bind(playlist, editedPlaylist) binding.detailType.text = binding.context.getString(R.string.lbl_playlist) binding.detailName.text = playlist.name.resolve(binding.context) diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index 395429104..ccc2be442 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -50,6 +50,7 @@ import okio.buffer import okio.source import org.oxycblt.auxio.image.CoverMode import org.oxycblt.auxio.image.ImageSettings +import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.util.logD @@ -81,7 +82,12 @@ constructor( } fun computeAlbumOrdering(songs: List) = - songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key } + Sort(Sort.Mode.ByAlbum, Sort.Direction.ASCENDING) + .songs(songs) + .groupBy { it.album } + .entries + .sortedByDescending { it.value.size } + .map { it.key } private suspend fun openInputStream(album: Album): InputStream? = try { diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index b2e4553dc..a9783cfae 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -341,8 +341,6 @@ interface Artist : MusicParent { * @author Alexander Capehart (OxygenCobalt) */ interface Genre : MusicParent { - /** The albums indirectly linked to by the [Song]s of this [Genre]. */ - val albums: List /** The artists indirectly linked to by the [Artist]s of this [Genre]. */ val artists: List /** The total duration of the songs in this genre, in milliseconds. */ @@ -355,8 +353,6 @@ interface Genre : MusicParent { * @author Alexander Capehart (OxygenCobalt) */ interface Playlist : MusicParent { - /** The albums indirectly linked to by the [Song]s of this [Playlist]. */ - val albums: List /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 204acad02..47328689f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -387,9 +387,9 @@ class ArtistImpl( } songs = distinctSongs.toList() - albums = albumMap.keys.toList() - explicitAlbums = albumMap.entries.filter { it.value }.map { it.key } - implicitAlbums = albumMap.entries.filterNot { it.value }.map { it.key } + albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(albumMap.keys) + explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) } + implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) } durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull() } @@ -436,7 +436,6 @@ class GenreImpl( rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } ?: Name.Unknown(R.string.def_genre) - override val albums: List override val artists: List override val durationMs: Long @@ -462,10 +461,6 @@ class GenreImpl( totalDuration += song.durationMs } - albums = - Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) - .albums(distinctAlbums) - .sortedByDescending { album -> album.songs.count { it.genres.contains(this) } } artists = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).artists(distinctArtists) durationMs = totalDuration } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index 1194ea028..929d121ad 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -33,8 +33,6 @@ private constructor( override val songs: List ) : Playlist { override val durationMs = songs.sumOf { it.durationMs } - override val albums = - songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key } /** * Clone the data in this instance to a new [PlaylistImpl] with the given [name]. From 699227c1a8a840f0d12e171fa6f4744d6017bd51 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 26 May 2023 16:26:31 -0600 Subject: [PATCH 13/43] all: relog project Fill in a lot of code paths in the project with log statements in order to improve the debugging experience. --- .../java/org/oxycblt/auxio/MainActivity.kt | 9 +- .../java/org/oxycblt/auxio/MainFragment.kt | 43 ++++++--- .../auxio/detail/AlbumDetailFragment.kt | 18 ++-- .../auxio/detail/ArtistDetailFragment.kt | 11 ++- .../auxio/detail/DetailAppBarLayout.kt | 13 ++- .../oxycblt/auxio/detail/DetailViewModel.kt | 43 +++++++-- .../auxio/detail/GenreDetailFragment.kt | 8 +- .../auxio/detail/PlaylistDetailFragment.kt | 35 +++++--- .../oxycblt/auxio/detail/SongDetailDialog.kt | 5 +- .../detail/header/DetailHeaderAdapter.kt | 3 + .../header/PlaylistDetailHeaderAdapter.kt | 11 ++- .../detail/list/AlbumDetailListAdapter.kt | 2 + .../auxio/detail/list/DetailListAdapter.kt | 1 - .../detail/list/PlaylistDetailListAdapter.kt | 2 + .../auxio/home/FlipFloatingActionButton.kt | 16 +++- .../org/oxycblt/auxio/home/HomeFragment.kt | 86 +++++++++++++----- .../org/oxycblt/auxio/home/HomeSettings.kt | 20 +++-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 15 +++- .../auxio/home/list/AlbumListFragment.kt | 6 +- .../auxio/home/list/ArtistListFragment.kt | 7 +- .../auxio/home/list/GenreListFragment.kt | 7 +- .../auxio/home/list/PlaylistListFragment.kt | 4 +- .../auxio/home/tabs/AdaptiveTabStrategy.kt | 16 +--- .../java/org/oxycblt/auxio/home/tabs/Tab.kt | 9 ++ .../org/oxycblt/auxio/home/tabs/TabAdapter.kt | 4 + .../auxio/home/tabs/TabCustomizeDialog.kt | 15 ++-- .../auxio/home/tabs/TabDragCallback.kt | 4 +- .../org/oxycblt/auxio/image/ImageSettings.kt | 1 + .../auxio/image/extractor/CoverExtractor.kt | 6 +- .../image/extractor/SquareFrameTransform.kt | 3 +- .../main/java/org/oxycblt/auxio/list/Data.kt | 1 + .../org/oxycblt/auxio/list/ListFragment.kt | 59 ++++++++++--- .../main/java/org/oxycblt/auxio/list/Sort.kt | 2 - .../auxio/list/adapter/FlexibleListAdapter.kt | 14 ++- .../list/adapter/PlayingIndicatorAdapter.kt | 2 + .../list/adapter/SelectionIndicatorAdapter.kt | 2 + .../list/recycler/MaterialDragCallback.kt | 6 +- .../list/selection/SelectionViewModel.kt | 24 ++++- .../oxycblt/auxio/music/MusicRepository.kt | 35 ++++++-- .../org/oxycblt/auxio/music/MusicSettings.kt | 11 ++- .../org/oxycblt/auxio/music/MusicViewModel.kt | 16 ++++ .../auxio/music/cache/CacheRepository.kt | 8 +- .../auxio/music/device/DeviceLibrary.kt | 3 + .../auxio/music/device/DeviceMusicImpl.kt | 7 ++ .../auxio/music/fs/DirectoryAdapter.kt | 12 +-- .../java/org/oxycblt/auxio/music/fs/Fs.kt | 12 +-- .../auxio/music/fs/MediaStoreExtractor.kt | 6 +- .../java/org/oxycblt/auxio/music/info/Date.kt | 43 +++++---- .../java/org/oxycblt/auxio/music/info/Disc.kt | 1 + .../java/org/oxycblt/auxio/music/info/Name.kt | 1 + .../auxio/music/metadata/AudioProperties.kt | 30 +++---- .../auxio/music/metadata/SeparatorsDialog.kt | 5 +- .../auxio/music/metadata/TagExtractor.kt | 5 ++ .../oxycblt/auxio/music/metadata/TagWorker.kt | 6 +- .../auxio/music/picker/AddToPlaylistDialog.kt | 3 +- .../music/picker/DeletePlaylistDialog.kt | 3 +- .../auxio/music/picker/NewPlaylistDialog.kt | 2 + .../music/picker/PlaylistPickerViewModel.kt | 78 +++++++++++++---- .../music/picker/RenamePlaylistDialog.kt | 5 +- .../auxio/music/system/IndexerService.kt | 7 ++ .../oxycblt/auxio/music/user/PlaylistImpl.kt | 2 + .../oxycblt/auxio/music/user/UserLibrary.kt | 68 +++++++++++++-- .../auxio/navigation/NavigationViewModel.kt | 4 +- .../picker/NavigateToArtistDialog.kt | 2 +- .../picker/NavigationPickerViewModel.kt | 18 +++- .../auxio/playback/PlaybackBarFragment.kt | 21 +++-- .../auxio/playback/PlaybackPanelFragment.kt | 3 + .../auxio/playback/PlaybackSettings.kt | 10 ++- .../auxio/playback/PlaybackViewModel.kt | 87 ++++++++++++++++--- .../playback/persist/PersistenceRepository.kt | 15 ++-- .../playback/picker/PlayFromArtistDialog.kt | 2 + .../playback/picker/PlayFromGenreDialog.kt | 2 + .../picker/PlaybackPickerViewModel.kt | 6 ++ .../org/oxycblt/auxio/playback/queue/Queue.kt | 53 +++++++++-- .../auxio/playback/queue/QueueAdapter.kt | 8 ++ .../auxio/playback/queue/QueueFragment.kt | 7 +- .../auxio/playback/queue/QueueViewModel.kt | 12 +++ .../replaygain/PreAmpCustomizeDialog.kt | 2 + .../replaygain/ReplayGainAudioProcessor.kt | 15 +++- .../playback/state/PlaybackStateManager.kt | 32 +++++-- .../playback/system/MediaButtonReceiver.kt | 2 + .../playback/system/MediaSessionComponent.kt | 33 +++++-- .../playback/system/NotificationComponent.kt | 7 ++ .../auxio/playback/system/PlaybackService.kt | 56 +++++++++--- .../playback/ui/AnimatedMaterialButton.kt | 3 + .../auxio/playback/ui/StyledSeekBar.kt | 1 + .../org/oxycblt/auxio/search/SearchEngine.kt | 7 +- .../oxycblt/auxio/search/SearchFragment.kt | 5 ++ .../oxycblt/auxio/search/SearchViewModel.kt | 13 ++- .../oxycblt/auxio/settings/AboutFragment.kt | 22 +++-- .../auxio/settings/BasePreferenceFragment.kt | 6 +- .../auxio/settings/RootPreferenceFragment.kt | 8 ++ .../categories/AudioPreferenceFragment.kt | 2 + .../categories/MusicPreferenceFragment.kt | 4 + .../PersonalizePreferenceFragment.kt | 2 + .../categories/UIPreferenceFragment.kt | 7 ++ .../auxio/ui/BaseBottomSheetBehavior.kt | 2 + .../auxio/ui/BottomSheetContentBehavior.kt | 3 + .../auxio/ui/CoordinatorAppBarLayout.kt | 2 + .../java/org/oxycblt/auxio/ui/MultiToolbar.kt | 15 ++-- .../java/org/oxycblt/auxio/ui/UISettings.kt | 1 + .../org/oxycblt/auxio/util/FrameworkUtil.kt | 2 +- .../oxycblt/auxio/widgets/WidgetComponent.kt | 5 ++ .../oxycblt/auxio/widgets/WidgetProvider.kt | 2 + app/src/main/res/values/strings.xml | 1 - .../java/org/oxycblt/auxio/music/FakeMusic.kt | 2 - 106 files changed, 1068 insertions(+), 346 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index d29b513a6..62e6a7c23 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -36,6 +36,7 @@ import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.systemBarInsetsCompat /** @@ -121,6 +122,7 @@ class MainActivity : AppCompatActivity() { private fun startIntentAction(intent: Intent?): Boolean { if (intent == null) { // Nothing to do. + logD("No intent to handle") return false } @@ -129,6 +131,7 @@ class MainActivity : AppCompatActivity() { // This is because onStart can run multiple times, and thus we really don't // want to return false and override the original delayed action with a // RestoreState action. + logD("Already used this intent") return true } intent.putExtra(KEY_INTENT_USED, true) @@ -137,8 +140,12 @@ class MainActivity : AppCompatActivity() { when (intent.action) { Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false) Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll - else -> return false + else -> { + logW("Unexpected intent ${intent.action}") + return false + } } + logD("Translated intent to $action") playbackModel.startAction(action) return true } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index e3abffd85..cfa4bd7b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -57,6 +57,7 @@ import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimen +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.unlikelyToBeNull @@ -66,6 +67,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * high-level navigation features. * * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Break up the god navigation setup going on here */ @AndroidEntryPoint class MainFragment : @@ -115,9 +118,11 @@ class MainFragment : val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? if (queueSheetBehavior != null) { - // Bottom sheet mode, set up click listeners. + // In portrait mode, set up click listeners on the stacked sheets. + logD("Configuring stacked bottom sheets") val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior + // TODO: Use the material handle unlikelyToBeNull(binding.handleWrapper).setOnClickListener { if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED && queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) { @@ -127,6 +132,7 @@ class MainFragment : } } else { // Dual-pane mode, manually style the static queue sheet. + logD("Configuring dual-pane bottom sheet") binding.queueSheet.apply { // Emulate the elevated bottom sheet style. background = @@ -280,19 +286,15 @@ class MainFragment : } private fun handleMainNavigation(action: MainNavigationAction?) { - if (action == null) { - // Nothing to do. - return + if (action != null) { + when (action) { + is MainNavigationAction.OpenPlaybackPanel -> tryOpenPlaybackPanel() + is MainNavigationAction.ClosePlaybackPanel -> tryClosePlaybackPanel() + is MainNavigationAction.Directions -> + findNavController().navigateSafe(action.directions) + } + navModel.mainNavigationAction.consume() } - - when (action) { - is MainNavigationAction.OpenPlaybackPanel -> tryOpenPlaybackPanel() - is MainNavigationAction.ClosePlaybackPanel -> tryClosePlaybackPanel() - is MainNavigationAction.Directions -> - findNavController().navigateSafe(action.directions) - } - - navModel.mainNavigationAction.consume() } private fun handleExploreNavigation(item: Music?) { @@ -377,6 +379,7 @@ class MainFragment : if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) { // Playback sheet is not expanded and not hidden, we can expand it. + logD("Expanding playback sheet") playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED return } @@ -387,6 +390,7 @@ class MainFragment : queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { // Queue sheet and playback sheet is expanded, close the queue sheet so the // playback panel can eb shown. + logD("Collapsing queue sheet") queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED } } @@ -397,6 +401,7 @@ class MainFragment : binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { // Playback sheet (and possibly queue) needs to be collapsed. + logD("Closing playback and queue sheets") val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED @@ -409,6 +414,7 @@ class MainFragment : val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_HIDDEN) { + logD("Unhiding and enabling playback sheet") val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? // Queue sheet behavior is either collapsed or expanded, no hiding needed @@ -429,6 +435,8 @@ class MainFragment : val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? + logD("Hiding and disabling playback and queue sheets") + // Make both bottom sheets non-draggable so the user can't halt the hiding event. queueSheetBehavior?.apply { isDraggable = false @@ -458,6 +466,7 @@ class MainFragment : if (queueSheetBehavior != null && queueSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED && playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { + logD("Hiding queue sheet") queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED return } @@ -465,21 +474,25 @@ class MainFragment : // If expanded, collapse the playback sheet next. if (playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED && playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) { + logD("Hiding playback sheet") playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED return } // Clear out pending playlist edits. if (detailModel.dropPlaylistEdit()) { + logD("Dropping playlist edits") return } // Clear out any prior selections. if (selectionModel.drop()) { + logD("Dropping selection") return } // Then try to navigate out of the explore navigation fragments (i.e Detail Views) + logD("Navigate away from explore view") binding.exploreNavHost.findNavController().navigateUp() } @@ -500,6 +513,10 @@ class MainFragment : binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? val exploreNavController = binding.exploreNavHost.findNavController() + // TODO: Debug why this fails sometimes on the playback sheet + // TODO: Add playlist editing + // TODO: Can this be split up? + isEnabled = queueSheetBehavior?.state == BackportBottomSheetBehavior.STATE_EXPANDED || playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED || 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 d5932974e..d32f5254b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -55,6 +55,7 @@ import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.share @@ -168,7 +169,10 @@ class AlbumDetailFragment : requireContext().share(currentAlbum) true } - else -> false + else -> { + logW("Unexpected menu item selected") + false + } } } @@ -222,7 +226,7 @@ class AlbumDetailFragment : private fun updateAlbum(album: Album?) { if (album == null) { - // Album we were showing no longer exists. + logD("No album to show, navigating away") findNavController().navigateUp() return } @@ -231,12 +235,8 @@ class AlbumDetailFragment : } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) { - albumListAdapter.setPlaying(song, isPlaying) - } else { - // Clear the ViewHolders if the mode isn't ALL_SONGS - albumListAdapter.setPlaying(null, isPlaying) - } + albumListAdapter.setPlaying( + song.takeIf { parent == detailModel.currentAlbum.value }, isPlaying) } private fun handleNavigation(item: Music?) { @@ -303,7 +303,7 @@ class AlbumDetailFragment : boxStart: Int, boxEnd: Int, snapPreference: Int - ): Int = + ) = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2) } 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 5384ead4c..601c2ed50 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -52,6 +52,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.share @@ -164,7 +165,10 @@ class ArtistDetailFragment : requireContext().share(currentArtist) true } - else -> false + else -> { + logW("Unexpected menu item selected") + false + } } } @@ -233,7 +237,7 @@ class ArtistDetailFragment : private fun updateArtist(artist: Artist?) { if (artist == null) { - // Artist we were showing no longer exists. + logD("No artist to show, navigating away") findNavController().navigateUp() return } @@ -242,6 +246,9 @@ class ArtistDetailFragment : // Disable options that make no sense with an empty artist val playable = artist.songs.isNotEmpty() + if (!playable) { + logD("Artist is empty, disabling playback/playlist/share options") + } menu.findItem(R.id.action_play_next).isEnabled = playable menu.findItem(R.id.action_queue_add).isEnabled = playable menu.findItem(R.id.action_playlist_add).isEnabled = playable diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt index 15b803ae6..28c1f65f7 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -35,6 +35,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.CoordinatorAppBarLayout import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.lazyReflectedField +import org.oxycblt.auxio.util.logD /** * An [CoordinatorAppBarLayout] that displays the title of a hidden [Toolbar] when the scrolling @@ -77,7 +78,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr (TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply { // We can never properly initialize the title view's state before draw time, // so we just set it's alpha to 0f to produce a less jarring initialization - // animation.. + // animation. alpha = 0f } @@ -101,12 +102,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (titleShown == visible) return titleShown = visible - val titleAnimator = titleAnimator - if (titleAnimator != null) { - titleAnimator.cancel() - this.titleAnimator = null - } - // Emulate the AppBarLayout lift animation (Linear, alpha 0f -> 1f), but now with // the title view's alpha instead of the AppBarLayout's elevation. val titleView = findTitleView() @@ -126,7 +121,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr return } - this.titleAnimator = + logD("Changing title visibility [from: $from to: $to]") + titleAnimator?.cancel() + titleAnimator = ValueAnimator.ofFloat(from, to).apply { addUpdateListener { titleView.alpha = it.animatedValue as Float } duration = diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 0b1861f8e..f18321430 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -53,6 +53,7 @@ import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW /** * [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the @@ -229,9 +230,9 @@ constructor( if (changes.userLibrary && userLibrary != null) { val playlist = currentPlaylist.value if (playlist != null) { - logD("Updated playlist to ${currentPlaylist.value}") _currentPlaylist.value = userLibrary.findPlaylist(playlist.uid)?.also(::refreshPlaylistList) + logD("Updated playlist to ${currentPlaylist.value}") } } } @@ -243,8 +244,11 @@ constructor( * @param uid The UID of the [Song] to load. Must be valid. */ fun setSong(uid: Music.UID) { - logD("Opening Song [uid: $uid]") + logD("Opening song $uid") _currentSong.value = musicRepository.deviceLibrary?.findSong(uid)?.also(::refreshAudioInfo) + if (_currentSong.value == null) { + logW("Given song UID was invalid") + } } /** @@ -254,9 +258,12 @@ constructor( * @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid. */ fun setAlbum(uid: Music.UID) { - logD("Opening Album [uid: $uid]") + logD("Opening album $uid") _currentAlbum.value = musicRepository.deviceLibrary?.findAlbum(uid)?.also(::refreshAlbumList) + if (_currentAlbum.value == null) { + logW("Given album UID was invalid") + } } /** @@ -266,9 +273,12 @@ constructor( * @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid. */ fun setArtist(uid: Music.UID) { - logD("Opening Artist [uid: $uid]") + logD("Opening artist $uid") _currentArtist.value = musicRepository.deviceLibrary?.findArtist(uid)?.also(::refreshArtistList) + if (_currentArtist.value == null) { + logW("Given artist UID was invalid") + } } /** @@ -278,9 +288,12 @@ constructor( * @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid. */ fun setGenre(uid: Music.UID) { - logD("Opening Genre [uid: $uid]") + logD("Opening genre $uid") _currentGenre.value = musicRepository.deviceLibrary?.findGenre(uid)?.also(::refreshGenreList) + if (_currentGenre.value == null) { + logW("Given genre UID was invalid") + } } /** @@ -290,9 +303,12 @@ constructor( * @param uid The [Music.UID] of the [Playlist] to update [currentPlaylist] to. Must be valid. */ fun setPlaylist(uid: Music.UID) { - logD("Opening Playlist [uid: $uid]") + logD("Opening playlist $uid") _currentPlaylist.value = musicRepository.userLibrary?.findPlaylist(uid)?.also(::refreshPlaylistList) + if (_currentPlaylist.value == null) { + logW("Given playlist UID was invalid") + } } /** Start a playlist editing session. Does nothing if a playlist is not being shown. */ @@ -310,6 +326,7 @@ constructor( fun savePlaylistEdit() { val playlist = _currentPlaylist.value ?: return val editedPlaylist = _editedPlaylist.value ?: return + logD("Committing playlist edits") viewModelScope.launch { musicRepository.rewritePlaylist(playlist, editedPlaylist) // TODO: The user could probably press some kind of button if they were fast enough. @@ -330,6 +347,7 @@ constructor( // Nothing to do. return false } + logD("Discarding playlist edits") _editedPlaylist.value = null refreshPlaylistList(playlist) return true @@ -351,6 +369,7 @@ constructor( if (realFrom !in editedPlaylist.indices || realTo !in editedPlaylist.indices) { return false } + logD("Moving playlist song from $realFrom [$from] to $realTo [$to]") editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo)) _editedPlaylist.value = editedPlaylist refreshPlaylistList(playlist, UpdateInstructions.Move(from, to)) @@ -369,6 +388,7 @@ constructor( if (realAt !in editedPlaylist.indices) { return } + logD("Removing playlist song at $realAt [$at]") editedPlaylist.removeAt(realAt) _editedPlaylist.value = editedPlaylist refreshPlaylistList( @@ -376,11 +396,13 @@ constructor( if (editedPlaylist.isNotEmpty()) { UpdateInstructions.Remove(at, 1) } else { + logD("Playlist will be empty after removal, removing header") UpdateInstructions.Remove(at - 2, 3) }) } private fun refreshAudioInfo(song: Song) { + logD("Refreshing audio info") // Clear any previous job in order to avoid stale data from appearing in the UI. currentSongJob?.cancel() _songAudioProperties.value = null @@ -388,6 +410,7 @@ constructor( viewModelScope.launch(Dispatchers.IO) { val info = audioPropertiesFactory.extract(song) yield() + logD("Updating audio info to $info") _songAudioProperties.value = info } } @@ -421,6 +444,7 @@ constructor( list.addAll(songs) } + logD("Update album list to ${list.size} items with $instructions") _albumInstructions.put(instructions) _albumList.value = list } @@ -454,6 +478,7 @@ constructor( // groupByTo normally returns a mapping to a MutableList mapping. Since MutableList // inherits list, we can cast upwards and save a copy by directly inserting the // implicit album list into the mapping. + logD("Implicit albums present, adding to list") @Suppress("UNCHECKED_CAST") (grouping as MutableMap>)[AlbumGrouping.APPEARANCES] = artist.implicitAlbums @@ -482,6 +507,7 @@ constructor( list.addAll(artistSongSort.songs(artist.songs)) } + logD("Updating artist list to ${list.size} items with $instructions") _artistInstructions.put(instructions) _artistList.value = list.toList() } @@ -500,12 +526,14 @@ constructor( list.add(songHeader) val instructions = if (replace) { - // Intentional so that the header item isn't replaced with the songs + // Intentional so that the header item isn't replaced alongside the songs UpdateInstructions.Replace(list.size) } else { UpdateInstructions.Diff } list.addAll(genreSongSort.songs(genre.songs)) + + logD("Updating genre list to ${list.size} items with $instructions") _genreInstructions.put(instructions) _genreList.value = list } @@ -525,6 +553,7 @@ constructor( list.addAll(songs) } + logD("Updating playlist list to ${list.size} items with $instructions") _playlistInstructions.put(instructions) _playlistList.value = list } 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 c178f4a1a..3968c1379 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -53,6 +53,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.share @@ -163,7 +164,10 @@ class GenreDetailFragment : requireContext().share(currentGenre) true } - else -> false + else -> { + logW("Unexpected menu item selected") + false + } } } @@ -230,7 +234,7 @@ class GenreDetailFragment : private fun updatePlaylist(genre: Genre?) { if (genre == null) { - // Genre we were showing no longer exists. + logD("No genre to show, navigating away") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 32044be87..7cdf9443c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -56,6 +56,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.share @@ -218,7 +219,10 @@ class PlaylistDetailFragment : detailModel.savePlaylistEdit() true } - else -> false + else -> { + logW("Unexpected menu item selected") + false + } } } @@ -259,6 +263,9 @@ class PlaylistDetailFragment : title = playlist.name.resolve(requireContext()) // Disable options that make no sense with an empty playlist val playable = playlist.songs.isNotEmpty() + if (!playable) { + logD("Playlist is empty, disabling playback/share options") + } menu.findItem(R.id.action_play_next).isEnabled = playable menu.findItem(R.id.action_queue_add).isEnabled = playable menu.findItem(R.id.action_share).isEnabled = playable @@ -269,13 +276,9 @@ class PlaylistDetailFragment : } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - // Prefer songs that might be playing from this playlist. - if (parent is Playlist && - parent.uid == unlikelyToBeNull(detailModel.currentPlaylist.value).uid) { - playlistListAdapter.setPlaying(song, isPlaying) - } else { - playlistListAdapter.setPlaying(null, isPlaying) - } + // Prefer songs that are playing from this playlist. + playlistListAdapter.setPlaying( + song.takeIf { parent == detailModel.currentPlaylist.value }, isPlaying) } private fun handleNavigation(item: Music?) { @@ -312,6 +315,7 @@ class PlaylistDetailFragment : selectionModel.drop() if (editedPlaylist != null) { + logD("Updating save button state") requireBinding().detailEditToolbar.menu.findItem(R.id.action_save).apply { isEnabled = editedPlaylist != detailModel.currentPlaylist.value?.songs } @@ -333,9 +337,18 @@ class PlaylistDetailFragment : private fun updateMultiToolbar() { val id = when { - detailModel.editedPlaylist.value != null -> R.id.detail_edit_toolbar - selectionModel.selected.value.isNotEmpty() -> R.id.detail_selection_toolbar - else -> R.id.detail_normal_toolbar + detailModel.editedPlaylist.value != null -> { + logD("Currently editing playlist, showing edit toolbar") + R.id.detail_edit_toolbar + } + selectionModel.selected.value.isNotEmpty() -> { + logD("Currently selecting, showing selection toolbar") + R.id.detail_selection_toolbar + } + else -> { + logD("Using normal toolbar") + R.id.detail_normal_toolbar + } } requireBinding().detailToolbar.setVisible(id) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index 5ba78ea8f..ca38c061e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -41,6 +41,7 @@ import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.concatLocalized +import org.oxycblt.auxio.util.logD /** * A [ViewBindingDialogFragment] that shows information about a Song. @@ -73,7 +74,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { private fun updateSong(song: Song?, info: AudioProperties?) { if (song == null) { - // Song we were showing no longer exists. + logD("No song to show, navigating away") findNavController().navigateUp() return } @@ -86,7 +87,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { add(SongProperty(R.string.lbl_album, song.album.zipName(context))) add(SongProperty(R.string.lbl_artists, song.artists.zipNames(context))) add(SongProperty(R.string.lbl_genres, song.genres.resolveNames(context))) - song.date?.let { add(SongProperty(R.string.lbl_date, it.resolveDate(context))) } + song.date?.let { add(SongProperty(R.string.lbl_date, it.resolve(context))) } song.track?.let { add(SongProperty(R.string.lbl_track, getString(R.string.fmt_number, it))) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt index 06317f5e2..247875432 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.detail.header import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.util.logD /** * A [RecyclerView.Adapter] that implements shared behavior between each parent header view. @@ -47,6 +48,7 @@ abstract class DetailHeaderAdapter { logD("Navigating to search") setupAxisTransitions(MaterialSharedAxis.Z) findNavController().navigateSafe(HomeFragmentDirections.actionShowSearch()) + true } R.id.action_settings -> { logD("Navigating to settings") navModel.mainNavigateTo( MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings())) + true } R.id.action_about -> { logD("Navigating to about") navModel.mainNavigateTo( MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout())) + true } // Handle sort menu R.id.submenu_sorting -> { // Junk click event when opening the menu + true } R.id.option_sort_asc -> { + logD("Switching to ascending sorting") item.isChecked = true homeModel.setSortForCurrentTab( homeModel .getSortForTab(homeModel.currentTabMode.value) .withDirection(Sort.Direction.ASCENDING)) + true } R.id.option_sort_dec -> { + logD("Switching to descending sorting") item.isChecked = true homeModel.setSortForCurrentTab( homeModel .getSortForTab(homeModel.currentTabMode.value) .withDirection(Sort.Direction.DESCENDING)) + true } else -> { - // Sorting option was selected, mark it as selected and update the mode - item.isChecked = true - homeModel.setSortForCurrentTab( - homeModel - .getSortForTab(homeModel.currentTabMode.value) - .withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))) + val newMode = Sort.Mode.fromItemId(item.itemId) + if (newMode != null) { + // Sorting option was selected, mark it as selected and update the mode + logD("Updating sort mode") + item.isChecked = true + homeModel.setSortForCurrentTab( + homeModel.getSortForTab(homeModel.currentTabMode.value).withMode(newMode)) + true + } else { + logW("Unexpected menu item selected") + false + } } } - - // Always handling it one way or another, so always return true - return true } private fun setupPager(binding: FragmentHomeBinding) { @@ -268,6 +280,7 @@ class HomeFragment : if (homeModel.currentTabModes.size == 1) { // A single tab makes the tab layout redundant, hide it and disable the collapsing // behavior. + logD("Single tab shown, disabling TabLayout") binding.homeTabs.isVisible = false binding.homeAppbar.setExpanded(true, false) toolbarParams.scrollFlags = 0 @@ -292,17 +305,26 @@ class HomeFragment : val isVisible: (Int) -> Boolean = when (tabMode) { // Disallow sorting by count for songs - MusicMode.SONGS -> { id -> id != R.id.option_sort_count } + MusicMode.SONGS -> { + logD("Using song-specific menu options") + ({ id -> id != R.id.option_sort_count }) + } // Disallow sorting by album for albums - MusicMode.ALBUMS -> { id -> id != R.id.option_sort_album } + MusicMode.ALBUMS -> { + logD("Using album-specific menu options") + ({ id -> id != R.id.option_sort_album }) + } // Only allow sorting by name, count, and duration for parents - else -> { id -> + else -> { + logD("Using parent-specific menu options") + ({ id -> id == R.id.option_sort_asc || id == R.id.option_sort_dec || id == R.id.option_sort_name || id == R.id.option_sort_count || id == R.id.option_sort_duration - } + }) + } } val sortMenu = @@ -310,18 +332,29 @@ class HomeFragment : val toHighlight = homeModel.getSortForTab(tabMode) for (option in sortMenu) { - // Check the ascending option and corresponding sort option to align with + val isCurrentMode = option.itemId == toHighlight.mode.itemId + val isCurrentlyAscending = + option.itemId == R.id.option_sort_asc && + toHighlight.direction == Sort.Direction.ASCENDING + val isCurrentlyDescending = + option.itemId == R.id.option_sort_dec && + toHighlight.direction == Sort.Direction.DESCENDING + // Check the corresponding direction and mode sort options to align with // the current sort of the tab. - if (option.itemId == toHighlight.mode.itemId || - (option.itemId == R.id.option_sort_asc && - toHighlight.direction == Sort.Direction.ASCENDING) || - (option.itemId == R.id.option_sort_dec && - toHighlight.direction == Sort.Direction.DESCENDING)) { + if (isCurrentMode || isCurrentlyAscending || isCurrentlyDescending) { + logD( + "Checking $option [mode: $isCurrentMode asc: $$isCurrentlyAscending dec: $isCurrentlyDescending]") + // Note: We cannot inline this boolean assignment since it unchecks all other radio + // buttons (even when setting it to false), which would result in nothing being + // selected. option.isChecked = true } // Disable options that are not allowed by the isVisible lambda option.isVisible = isVisible(option.itemId) + if (!option.isVisible) { + logD("Hiding $option") + } } // Update the scrolling view in AppBarLayout to align with the current tab's @@ -337,10 +370,12 @@ class HomeFragment : } if (tabMode != MusicMode.PLAYLISTS) { + logD("Flipping to shuffle button") binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) { playbackModel.shuffleAll() } } else { + logD("Flipping to playlist button") binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) { musicModel.createPlaylist() } @@ -350,6 +385,7 @@ class HomeFragment : private fun handleRecreate(recreate: Unit?) { if (recreate == null) return val binding = requireBinding() + logD("Recreating ViewPager") // Move back to position zero, as there must be a tab there. binding.homePager.currentItem = 0 // Make sure tabs are set up to also follow the new ViewPager configuration. @@ -386,7 +422,7 @@ class HomeFragment : binding.homeIndexingProgress.visibility = View.INVISIBLE when (error) { is NoAudioPermissionException -> { - logD("Updating UI to permission request state") + logD("Showing permission prompt") binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms) // Configure the action to act as a permission launcher. binding.homeIndexingAction.apply { @@ -401,7 +437,7 @@ class HomeFragment : } } is NoMusicException -> { - logD("Updating UI to no music state") + logD("Showing no music error") binding.homeIndexingStatus.text = context.getString(R.string.err_no_music) // Configure the action to act as a reload trigger. binding.homeIndexingAction.apply { @@ -411,7 +447,7 @@ class HomeFragment : } } else -> { - logD("Updating UI to error state") + logD("Showing generic error") binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed) // Configure the action to act as a reload trigger. binding.homeIndexingAction.apply { @@ -431,11 +467,13 @@ class HomeFragment : when (progress) { is IndexingProgress.Indeterminate -> { + logD("Showing generic progress") // In a query/initialization state, show a generic loading status. binding.homeIndexingStatus.text = getString(R.string.lng_indexing) binding.homeIndexingProgress.isIndeterminate = true } is IndexingProgress.Songs -> { + logD("Showing song progress") // Actively loading songs, show the current progress. binding.homeIndexingStatus.text = getString(R.string.fmt_indexing, progress.current, progress.total) @@ -454,8 +492,10 @@ class HomeFragment : // displaying the shuffle FAB makes no sense. We also don't want the fast scroll // popup to overlap with the FAB, so we hide the FAB when fast scrolling too. if (songs.isEmpty() || isFastScrolling) { + logD("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling") binding.homeFab.hide() } else { + logD("Showing fab") binding.homeFab.show() } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt index 60d3144e7..4e468ec95 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt @@ -26,6 +26,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -67,15 +68,18 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) override fun migrate() { if (sharedPreferences.contains(OLD_KEY_LIB_TABS)) { + logD("Migrating tab setting") val oldTabs = Tab.fromIntCode(sharedPreferences.getInt(OLD_KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT)) ?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT)) + logD("Old tabs: $oldTabs") // The playlist tab is now parsed, but it needs to be made visible. val playlistIndex = oldTabs.indexOfFirst { it.mode == MusicMode.PLAYLISTS } - if (playlistIndex > -1) { // Sanity check - oldTabs[playlistIndex] = Tab.Visible(MusicMode.PLAYLISTS) - } + check(playlistIndex > -1) // This should exist, otherwise we are in big trouble + oldTabs[playlistIndex] = Tab.Visible(MusicMode.PLAYLISTS) + logD("New tabs: $oldTabs") + sharedPreferences.edit { putInt(getString(R.string.set_key_home_tabs), Tab.toIntCode(oldTabs)) remove(OLD_KEY_LIB_TABS) @@ -85,8 +89,14 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) override fun onSettingChanged(key: String, listener: HomeSettings.Listener) { when (key) { - getString(R.string.set_key_home_tabs) -> listener.onTabsChanged() - getString(R.string.set_key_hide_collaborators) -> listener.onHideCollaboratorsChanged() + getString(R.string.set_key_home_tabs) -> { + logD("Dispatching tab setting change") + listener.onTabsChanged() + } + getString(R.string.set_key_hide_collaborators) -> { + logD("Dispatching collaborator setting change") + listener.onHideCollaboratorsChanged() + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index a584ace36..2099b1091 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -75,8 +75,7 @@ constructor( private val _artistsList = MutableStateFlow(listOf()) /** * A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that - * if "Hide collaborators" is on, this list will not include [Artist]s where - * [Artist.isCollaborator] is true. + * if "Hide collaborators" is on, this list will not include collaborator [Artist]s. */ val artistsList: MutableStateFlow> get() = _artistsList @@ -157,9 +156,11 @@ constructor( _artistsList.value = musicSettings.artistSort.artists( if (homeSettings.shouldHideCollaborators) { + logD("Filtering collaborator artists") // Hide Collaborators is enabled, filter out collaborators. deviceLibrary.artists.filter { it.explicitAlbums.isNotEmpty() } } else { + logD("Using all artists") deviceLibrary.artists }) _genresInstructions.put(UpdateInstructions.Diff) @@ -177,12 +178,14 @@ constructor( override fun onTabsChanged() { // Tabs changed, update the current tabs and set up a re-create event. currentTabModes = makeTabModes() + logD("Updating tabs: ${currentTabMode.value}") _shouldRecreate.put(Unit) } override fun onHideCollaboratorsChanged() { // Changes in the hide collaborator setting will change the artist contents // of the library, consider it a library update. + logD("Collaborator setting changed, forwarding update") onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) } @@ -207,30 +210,34 @@ constructor( * @param sort The new [Sort] to apply. Assumed to be an allowed sort for the current [Tab]. */ fun setSortForCurrentTab(sort: Sort) { - logD("Updating ${_currentTabMode.value} sort to $sort") // Can simply re-sort the current list of items without having to access the library. - when (_currentTabMode.value) { + when (val mode = _currentTabMode.value) { MusicMode.SONGS -> { + logD("Updating song [$mode] sort mode to $sort") musicSettings.songSort = sort _songsInstructions.put(UpdateInstructions.Replace(0)) _songsList.value = sort.songs(_songsList.value) } MusicMode.ALBUMS -> { + logD("Updating album [$mode] sort mode to $sort") musicSettings.albumSort = sort _albumsInstructions.put(UpdateInstructions.Replace(0)) _albumsLists.value = sort.albums(_albumsLists.value) } MusicMode.ARTISTS -> { + logD("Updating artist [$mode] sort mode to $sort") musicSettings.artistSort = sort _artistsInstructions.put(UpdateInstructions.Replace(0)) _artistsList.value = sort.artists(_artistsList.value) } MusicMode.GENRES -> { + logD("Updating genre [$mode] sort mode to $sort") musicSettings.genreSort = sort _genresInstructions.put(UpdateInstructions.Replace(0)) _genresList.value = sort.genres(_genresList.value) } MusicMode.PLAYLISTS -> { + logD("Updating playlist [$mode] sort mode to $sort") musicSettings.playlistSort = sort _playlistsInstructions.put(UpdateInstructions.Replace(0)) _playlistsList.value = sort.playlists(_playlistsList.value) 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 31b6b67bf..3495bc85a 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 @@ -107,7 +107,7 @@ class AlbumListFragment : is Sort.Mode.ByArtist -> album.artists[0].name.thumb // Date -> Use minimum date (Maximum dates are not sorted by, so showing them is odd) - is Sort.Mode.ByDate -> album.dates?.run { min.resolveDate(requireContext()) } + is Sort.Mode.ByDate -> album.dates?.run { min.resolve(requireContext()) } // Duration -> Use formatted duration is Sort.Mode.ByDuration -> album.durationMs.formatDurationMs(false) @@ -156,8 +156,8 @@ class AlbumListFragment : private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { // Only highlight the album if it is currently playing, and if the currently // playing song is also contained within. - val playlist = (parent as? Album)?.takeIf { song?.album == it } - albumAdapter.setPlaying(playlist, isPlaying) + val album = (parent as? Album)?.takeIf { song?.album == it } + albumAdapter.setPlaying(album, isPlaying) } /** 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 46d42f16c..e270fa7d2 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 @@ -44,7 +44,6 @@ import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.nonZeroOrNull /** @@ -123,7 +122,7 @@ class ArtistListFragment : } private fun updateArtists(artists: List) { - artistAdapter.update(artists, homeModel.artistsInstructions.consume().also { logD(it) }) + artistAdapter.update(artists, homeModel.artistsInstructions.consume()) } private fun updateSelection(selection: List) { @@ -133,8 +132,8 @@ class ArtistListFragment : private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { // Only highlight the artist if it is currently playing, and if the currently // playing song is also contained within. - val playlist = (parent as? Artist)?.takeIf { song?.run { artists.contains(it) } ?: false } - artistAdapter.setPlaying(playlist, isPlaying) + val artist = (parent as? Artist)?.takeIf { song?.run { artists.contains(it) } ?: false } + artistAdapter.setPlaying(artist, isPlaying) } /** 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 d592ba377..ee9544d55 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 @@ -44,7 +44,6 @@ import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD /** * A [ListFragment] that shows a list of [Genre]s. @@ -122,7 +121,7 @@ class GenreListFragment : } private fun updateGenres(genres: List) { - genreAdapter.update(genres, homeModel.genresInstructions.consume().also { logD(it) }) + genreAdapter.update(genres, homeModel.genresInstructions.consume()) } private fun updateSelection(selection: List) { @@ -132,8 +131,8 @@ class GenreListFragment : private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { // Only highlight the genre if it is currently playing, and if the currently // playing song is also contained within. - val playlist = (parent as? Genre)?.takeIf { song?.run { genres.contains(it) } ?: false } - genreAdapter.setPlaying(playlist, isPlaying) + val genre = (parent as? Genre)?.takeIf { song?.run { genres.contains(it) } ?: false } + genreAdapter.setPlaying(genre, isPlaying) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt index 167619c15..6a766661a 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt @@ -43,7 +43,6 @@ import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD /** * A [ListFragment] that shows a list of [Playlist]s. @@ -120,8 +119,7 @@ class PlaylistListFragment : } private fun updatePlaylists(playlists: List) { - playlistAdapter.update( - playlists, homeModel.playlistsInstructions.consume().also { logD(it) }) + playlistAdapter.update(playlists, homeModel.playlistsInstructions.consume()) } private fun updateSelection(selection: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt index 718c99855..36aed93bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt @@ -23,7 +23,6 @@ import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicMode -import org.oxycblt.auxio.util.logD /** * A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations @@ -67,20 +66,11 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List) : // Use expected sw* size thresholds when choosing a configuration. when { // On small screens, only display an icon. - width < 370 -> { - logD("Using icon-only configuration") - tab.setIcon(icon).setContentDescription(string) - } + width < 370 -> tab.setIcon(icon).setContentDescription(string) // On large screens, display an icon and text. - width < 600 -> { - logD("Using text-only configuration") - tab.setText(string) - } + width < 600 -> tab.setText(string) // On medium-size screens, display text. - else -> { - logD("Using icon-and-text configuration") - tab.setIcon(icon).setText(string) - } + else -> tab.setIcon(icon).setText(string) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index 30425e6d8..2fddd1b4a 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.home.tabs import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.util.logE +import org.oxycblt.auxio.util.logW /** * A representation of a library tab suitable for configuration. @@ -84,6 +85,10 @@ sealed class Tab(open val mode: MusicMode) { fun toIntCode(tabs: Array): Int { // Like when deserializing, make sure there are no duplicate tabs for whatever reason. val distinct = tabs.distinctBy { it.mode } + if (tabs.size != distinct.size) { + logW( + "Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]") + } var sequence = 0 var shift = MAX_SEQUENCE_IDX * 4 @@ -127,6 +132,10 @@ sealed class Tab(open val mode: MusicMode) { // Make sure there are no duplicate tabs val distinct = tabs.distinctBy { it.mode } + if (tabs.size != distinct.size) { + logW( + "Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]") + } // For safety, return null if we have an empty or larger-than-expected tab array. if (distinct.isEmpty() || distinct.size < MAX_SEQUENCE_IDX) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index 9e778cca1..277c0c39b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -28,6 +28,7 @@ import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.util.inflater +import org.oxycblt.auxio.util.logD /** * A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration. @@ -52,6 +53,7 @@ class TabAdapter(private val listener: EditClickListListener) : * @param newTabs The new array of tabs to show. */ fun submitTabs(newTabs: Array) { + logD("Force-updating tab information") tabs = newTabs @Suppress("NotifyDatasetChanged") notifyDataSetChanged() } @@ -63,6 +65,7 @@ class TabAdapter(private val listener: EditClickListListener) : * @param tab The new tab. */ fun setTab(at: Int, tab: Tab) { + logD("Updating tab [at: $at, tab: $tab]") tabs[at] = tab // Use a payload to avoid an item change animation. notifyItemChanged(at, PAYLOAD_TAB_CHANGED) @@ -75,6 +78,7 @@ class TabAdapter(private val listener: EditClickListListener) : * @param b The position of the second tab to swap. */ fun swapTabs(a: Int, b: Int) { + logD("Swapping tabs [a: $a, b: $b]") val tmp = tabs[b] tabs[b] = tabs[a] tabs[a] = tmp diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index dae73e93e..c7dadd8d2 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -91,14 +91,15 @@ class TabCustomizeDialog : // We will need the exact index of the tab to update on in order to // notify the adapter of the change. val index = tabAdapter.tabs.indexOfFirst { it.mode == item.mode } - val tab = tabAdapter.tabs[index] - tabAdapter.setTab( - index, - when (tab) { + val old = tabAdapter.tabs[index] + val new = + when (old) { // Invert the visibility of the tab - is Tab.Visible -> Tab.Invisible(tab.mode) - is Tab.Invisible -> Tab.Visible(tab.mode) - }) + is Tab.Visible -> Tab.Invisible(old.mode) + is Tab.Invisible -> Tab.Visible(old.mode) + } + logD("Flipping tab visibility [from: $old to: $new]") + tabAdapter.setTab(index, new) // Prevent the user from saving if all the tabs are Invisible, as that's an invalid state. (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt index 064d5f8dd..49af3b57b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt @@ -63,7 +63,9 @@ class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callbac return true } - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + throw IllegalStateException() + } // We use a custom drag handle, so disable the long press action. override fun isLongPressDragEnabled() = false diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt index 7f1aca57f..a4c13c4c9 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt @@ -73,6 +73,7 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context override fun onSettingChanged(key: String, listener: ImageSettings.Listener) { if (key == getString(R.string.set_key_cover_mode)) { + logD("Dispatching cover mode setting change") listener.onCoverModeChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index ccc2be442..5faed970e 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -53,8 +53,7 @@ import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.logE class CoverExtractor @Inject @@ -97,7 +96,7 @@ constructor( CoverMode.QUALITY -> extractQualityCover(album) } } catch (e: Exception) { - logW("Unable to extract album cover due to an error: $e") + logE("Unable to extract album cover due to an error: $e") null } @@ -154,7 +153,6 @@ constructor( } if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) { - logD("Front cover found") stream = ByteArrayInputStream(pic) break } else if (stream == null) { diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt index bdc48b49a..b8d9de4e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt @@ -31,8 +31,7 @@ import kotlin.math.min * @author Alexander Capehart (OxygenCobalt) */ class SquareFrameTransform : Transformation { - override val cacheKey: String - get() = "SquareFrameTransform" + override val cacheKey = "SquareFrameTransform" override suspend fun transform(input: Bitmap, size: Size): Bitmap { // Find the smaller dimension and then take a center portion of the image that diff --git a/app/src/main/java/org/oxycblt/auxio/list/Data.kt b/app/src/main/java/org/oxycblt/auxio/list/Data.kt index e41dd4149..8636e1579 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Data.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Data.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.list import androidx.annotation.StringRes +// TODO: Consider breaking this up into sealed classes for individual adapters /** A marker for something that is a RecyclerView item. Has no functionality on it's own. */ interface Item diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 3f2610c3f..bca4fb774 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -36,6 +36,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.navigation.MainNavigationAction import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast @@ -94,33 +95,40 @@ abstract class ListFragment : R.id.action_play_next -> { playbackModel.playNext(song) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_queue_add -> { playbackModel.addToQueue(song) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_go_artist -> { navModel.exploreNavigateToParentArtist(song) + true } R.id.action_go_album -> { navModel.exploreNavigateTo(song.album) + true } R.id.action_share -> { requireContext().share(song) + true } R.id.action_playlist_add -> { musicModel.addToPlaylist(song) + true } R.id.action_song_detail -> { navModel.mainNavigateTo( MainNavigationAction.Directions( MainFragmentDirections.actionShowDetails(song.uid))) + true } else -> { - error("Unexpected menu item selected") + logW("Unexpected menu item selected") + false } } - true } } } @@ -141,32 +149,39 @@ abstract class ListFragment : when (it.itemId) { R.id.action_play -> { playbackModel.play(album) + true } R.id.action_shuffle -> { playbackModel.shuffle(album) + true } R.id.action_play_next -> { playbackModel.playNext(album) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_queue_add -> { playbackModel.addToQueue(album) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_go_artist -> { navModel.exploreNavigateToParentArtist(album) + true } R.id.action_playlist_add -> { musicModel.addToPlaylist(album) + true } R.id.action_share -> { requireContext().share(album) + true } else -> { - error("Unexpected menu item selected") + logW("Unexpected menu item selected") + false } } - true } } } @@ -184,6 +199,9 @@ abstract class ListFragment : openMenu(anchor, menuRes) { val playable = artist.songs.isNotEmpty() + if (!playable) { + logD("Artist is empty, disabling playback/playlist/share options") + } menu.findItem(R.id.action_play).isEnabled = playable menu.findItem(R.id.action_shuffle).isEnabled = playable menu.findItem(R.id.action_play_next).isEnabled = playable @@ -195,29 +213,35 @@ abstract class ListFragment : when (it.itemId) { R.id.action_play -> { playbackModel.play(artist) + true } R.id.action_shuffle -> { playbackModel.shuffle(artist) + true } R.id.action_play_next -> { playbackModel.playNext(artist) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_queue_add -> { playbackModel.addToQueue(artist) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_playlist_add -> { musicModel.addToPlaylist(artist) + true } R.id.action_share -> { requireContext().share(artist) + true } else -> { - error("Unexpected menu item selected") + logW("Unexpected menu item selected") + false } } - true } } } @@ -238,29 +262,35 @@ abstract class ListFragment : when (it.itemId) { R.id.action_play -> { playbackModel.play(genre) + true } R.id.action_shuffle -> { playbackModel.shuffle(genre) + true } R.id.action_play_next -> { playbackModel.playNext(genre) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_queue_add -> { playbackModel.addToQueue(genre) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_playlist_add -> { musicModel.addToPlaylist(genre) + true } R.id.action_share -> { requireContext().share(genre) + true } else -> { - error("Unexpected menu item selected") + logW("Unexpected menu item selected") + false } } - true } } } @@ -288,32 +318,39 @@ abstract class ListFragment : when (it.itemId) { R.id.action_play -> { playbackModel.play(playlist) + true } R.id.action_shuffle -> { playbackModel.shuffle(playlist) + true } R.id.action_play_next -> { playbackModel.playNext(playlist) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_queue_add -> { playbackModel.addToQueue(playlist) requireContext().showToast(R.string.lng_queue_added) + true } R.id.action_rename -> { musicModel.renamePlaylist(playlist) + true } R.id.action_delete -> { musicModel.deletePlaylist(playlist) + true } R.id.action_share -> { requireContext().share(playlist) + true } else -> { - error("Unexpected menu item selected") + logW("Unexpected menu item selected") + false } } - true } } } @@ -332,6 +369,8 @@ abstract class ListFragment : return } + logD("Opening popup menu menu") + currentMenu = PopupMenu(requireContext(), anchor).apply { inflate(menuRes) diff --git a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt index 06f0e60e9..8a7203182 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt @@ -22,8 +22,6 @@ import androidx.annotation.IdRes import kotlin.math.max import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R -import org.oxycblt.auxio.list.Sort.Direction -import org.oxycblt.auxio.list.Sort.Mode import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt index 3bdf330d9..977c367c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import java.util.concurrent.Executor +import org.oxycblt.auxio.util.logD /** * A variant of ListDiffer with more flexible updates. @@ -46,15 +47,18 @@ abstract class FlexibleListAdapter( /** * Update the adapter with new data. * - * @param newData The new list of data to update with. + * @param newList The new list of data to update with. * @param instructions The [UpdateInstructions] to visually update the list with. * @param callback Called when the update is completed. May be done asynchronously. */ fun update( - newData: List, + newList: List, instructions: UpdateInstructions?, callback: (() -> Unit)? = null - ) = differ.update(newData, instructions, callback) + ) { + logD("Updating list to ${newList.size} items with $instructions") + differ.update(newList, instructions, callback) + } } /** @@ -165,6 +169,7 @@ private class FlexibleListDiffer( ) { // fast simple remove all if (newList.isEmpty()) { + logD("Short-circuiting diff to remove all") val countRemoved = oldList.size currentList = emptyList() // notify last, after list is updated @@ -175,6 +180,7 @@ private class FlexibleListDiffer( // fast simple first insert if (oldList.isEmpty()) { + logD("Short-circuiting diff to insert all") currentList = newList // notify last, after list is updated updateCallback.onInserted(0, newList.size) @@ -233,8 +239,10 @@ private class FlexibleListDiffer( throw AssertionError() } }) + mainThreadExecutor.execute { if (maxScheduledGeneration == runGeneration) { + logD("Applying calculated diff") currentList = newList result.dispatchUpdatesTo(updateCallback) callback?.invoke() diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt index 67fccceac..ab0db5fe3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt @@ -58,6 +58,8 @@ abstract class PlayingIndicatorAdapter( * @param isPlaying Whether playback is ongoing or paused. */ fun setPlaying(item: T?, isPlaying: Boolean) { + logD("Updating playing item [old: $currentItem new: $item]") + var updatedItem = false if (currentItem != item) { val oldItem = currentItem diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt index 641e8b2b3..9339f78fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt @@ -22,6 +22,7 @@ import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.util.logD /** * A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of @@ -54,6 +55,7 @@ abstract class SelectionIndicatorAdapter( // Nothing to do. return } + logD("Updating selection [old=${oldSelectedItems.size} new=${newSelectedItems.size}") selectedItems = newSelectedItems for (i in currentList.indices) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt index 0143fe15b..28112ca61 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt @@ -68,7 +68,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { // this is only done once when the item is initially picked up. // TODO: I think this is possible to improve with a raw ValueAnimator. if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) { - logD("Lifting item") + logD("Lifting ViewHolder") val bg = holder.background val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal) @@ -110,7 +110,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { // This function can be called multiple times, so only start the animation when the view's // translationZ is already non-zero. if (holder.root.translationZ != 0f) { - logD("Dropping item") + logD("Lifting ViewHolder") val bg = holder.background val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal) @@ -137,7 +137,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { // Long-press events are too buggy, only allow dragging with the handle. final override fun isLongPressDragEnabled() = false - /** Required [RecyclerView.ViewHolder] implementation that exposes the following. */ + /** Required [RecyclerView.ViewHolder] implementation that exposes required fields */ interface ViewHolder { /** Whether this [ViewHolder] can be moved right now. */ val enabled: Boolean diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index ef3f54f60..a5cfd776e 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -27,10 +27,12 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.util.logD /** * A [ViewModel] that manages the current selection. @@ -83,10 +85,19 @@ constructor( * @param music The [Music] item to select. */ fun select(music: Music) { + if (music is MusicParent && music.songs.isEmpty()) { + logD("Cannot select empty parent, ignoring operation") + return + } + val selected = _selected.value.toMutableList() if (!selected.remove(music)) { + logD("Adding $music to selection") selected.add(music) + } else { + logD("Removed $music from selection") } + _selected.value = selected } @@ -95,8 +106,9 @@ constructor( * * @return A list of [Song]s collated from each item selected. */ - fun take() = - _selected.value + fun take(): List { + logD("Taking selection") + return _selected.value .flatMap { when (it) { is Song -> listOf(it) @@ -106,12 +118,16 @@ constructor( is Playlist -> it.songs } } - .also { drop() } + .also { _selected.value = listOf() } + } /** * Clear the current selection. * * @return true if the prior selection was non-empty, false otherwise. */ - fun drop() = _selected.value.isNotEmpty().also { _selected.value = listOf() } + fun drop(): Boolean { + logD("Dropping selection [empty=${_selected.value.isEmpty()}]") + return _selected.value.isNotEmpty().also { _selected.value = listOf() } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 56014b7f9..b86244241 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -34,9 +34,6 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield -import org.oxycblt.auxio.music.MusicRepository.IndexingListener -import org.oxycblt.auxio.music.MusicRepository.IndexingWorker -import org.oxycblt.auxio.music.MusicRepository.UpdateListener import org.oxycblt.auxio.music.cache.CacheRepository import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.RawSong @@ -55,6 +52,8 @@ import org.oxycblt.auxio.util.logW * music (loading) can be reacted to with [UpdateListener] and [IndexingListener]. * * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Switch listener to set */ interface MusicRepository { /** The current music information found on the device. */ @@ -289,36 +288,42 @@ constructor( override suspend fun createPlaylist(name: String, songs: List) { val userLibrary = synchronized(this) { userLibrary ?: return } + logD("Creating playlist $name with ${songs.size} songs") userLibrary.createPlaylist(name, songs) notifyUserLibraryChange() } override suspend fun renamePlaylist(playlist: Playlist, name: String) { val userLibrary = synchronized(this) { userLibrary ?: return } + logD("Renaming $playlist to $name") userLibrary.renamePlaylist(playlist, name) notifyUserLibraryChange() } override suspend fun deletePlaylist(playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } + logD("Deleting $playlist") userLibrary.deletePlaylist(playlist) notifyUserLibraryChange() } override suspend fun addToPlaylist(songs: List, playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } + logD("Adding ${songs.size} songs to $playlist") userLibrary.addToPlaylist(playlist, songs) notifyUserLibraryChange() } override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { val userLibrary = synchronized(this) { userLibrary ?: return } + logD("Rewriting $playlist with ${songs.size} songs") userLibrary.rewritePlaylist(playlist, songs) notifyUserLibraryChange() } @Synchronized private fun notifyUserLibraryChange() { + logD("Dispatching user library change") for (listener in updateListeners) { listener.onMusicChanges( MusicRepository.Changes(deviceLibrary = false, userLibrary = true)) @@ -327,6 +332,7 @@ constructor( @Synchronized override fun requestIndex(withCache: Boolean) { + logD("Requesting index operation [cache=$withCache]") indexingWorker?.requestIndex(withCache) } @@ -353,7 +359,7 @@ constructor( private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) { if (ContextCompat.checkSelfPermission(worker.context, PERMISSION_READ_AUDIO) == PackageManager.PERMISSION_DENIED) { - logE("Permission check failed") + logE("Permissions were not granted") // No permissions, signal that we can't do anything. throw NoAudioPermissionException() } @@ -363,14 +369,16 @@ constructor( emitLoading(IndexingProgress.Indeterminate) // Do the initial query of the cache and media databases in parallel. - logD("Starting queries") + logD("Starting MediaStore query") val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() } val cache = if (withCache) { + logD("Reading cache") cacheRepository.readCache() } else { null } + logD("Awaiting MediaStore query") val query = mediaStoreQueryJob.await().getOrThrow() // Now start processing the queried song information in parallel. Songs that can't be @@ -379,11 +387,13 @@ constructor( logD("Starting song discovery") val completeSongs = Channel(Channel.UNLIMITED) val incompleteSongs = Channel(Channel.UNLIMITED) + logD("Started MediaStore discovery") val mediaStoreJob = worker.scope.tryAsync { mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs) incompleteSongs.close() } + logD("Started ExoPlayer discovery") val metadataJob = worker.scope.tryAsync { tagExtractor.consume(incompleteSongs, completeSongs) @@ -396,7 +406,8 @@ constructor( rawSongs.add(rawSong) emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) } - // These should be no-ops + logD("Awaiting discovery completion") + // These should be no-ops, but we need the error state to see if we should keep going. mediaStoreJob.await().getOrThrow() metadataJob.await().getOrThrow() @@ -411,25 +422,35 @@ constructor( // TODO: Indicate playlist state in loading process? emitLoading(IndexingProgress.Indeterminate) val deviceLibraryChannel = Channel() + logD("Starting DeviceLibrary creation") val deviceLibraryJob = worker.scope.tryAsync(Dispatchers.Main) { deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) } } + logD("Starting UserLibrary creation") val userLibraryJob = worker.scope.tryAsync { userLibraryFactory.read(deviceLibraryChannel).also { deviceLibraryChannel.close() } } if (cache == null || cache.invalidated) { + logD("Writing cache [why=${cache?.invalidated}]") cacheRepository.writeCache(rawSongs) } + logD("Awaiting library creation") val deviceLibrary = deviceLibraryJob.await().getOrThrow() val userLibrary = userLibraryJob.await().getOrThrow() + + logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]") withContext(Dispatchers.Main) { emitComplete(null) emitData(deviceLibrary, userLibrary) } } + /** + * An extension of [async] that forces the outcome to a [Result] to allow exceptions to bubble + * upwards instead of crashing the entire app. + */ private inline fun CoroutineScope.tryAsync( context: CoroutineContext = EmptyCoroutineContext, crossinline block: suspend () -> R @@ -457,6 +478,7 @@ constructor( synchronized(this) { previousCompletedState = IndexingState.Completed(error) currentIndexingState = null + logD("Dispatching completion state [error=$error]") for (listener in indexingListeners) { listener.onIndexingStateChanged() } @@ -472,6 +494,7 @@ constructor( this.deviceLibrary = deviceLibrary this.userLibrary = userLibrary val changes = MusicRepository.Changes(deviceLibraryChanged, userLibraryChanged) + logD("Dispatching library change [changes=$changes]") for (listener in updateListeners) { listener.onMusicChanges(changes) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index 48b180388..4274ef4e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.fs.Directory import org.oxycblt.auxio.music.fs.MusicDirectories import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.getSystemServiceCompat +import org.oxycblt.auxio.util.logD /** * User configuration specific to music system. @@ -231,8 +232,14 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context getString(R.string.set_key_music_dirs), getString(R.string.set_key_music_dirs_include), getString(R.string.set_key_separators), - getString(R.string.set_key_auto_sort_names) -> listener.onIndexingSettingChanged() - getString(R.string.set_key_observing) -> listener.onObservingChanged() + getString(R.string.set_key_auto_sort_names) -> { + logD("Dispatching indexing setting change for $key") + listener.onIndexingSettingChanged() + } + getString(R.string.set_key_observing) -> { + logD("Dispatching observing setting change") + listener.onObservingChanged() + } } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index d207bd135..6390929b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent +import org.oxycblt.auxio.util.logD /** * A [ViewModel] providing data specific to the music loading process. @@ -89,6 +90,7 @@ constructor( deviceLibrary.artists.size, deviceLibrary.genres.size, deviceLibrary.songs.sumOf { it.durationMs }) + logD("Updated statistics: ${_statistics.value}") } override fun onIndexingStateChanged() { @@ -97,11 +99,13 @@ constructor( /** Requests that the music library should be re-loaded while leveraging the cache. */ fun refresh() { + logD("Refreshing library") musicRepository.requestIndex(true) } /** Requests that the music library be re-loaded without the cache. */ fun rescan() { + logD("Rescanning library") musicRepository.requestIndex(false) } @@ -113,8 +117,10 @@ constructor( */ fun createPlaylist(name: String? = null, songs: List = listOf()) { if (name != null) { + logD("Creating $name with ${songs.size} songs]") viewModelScope.launch(Dispatchers.IO) { musicRepository.createPlaylist(name, songs) } } else { + logD("Launching creation dialog for ${songs.size} songs") _newPlaylistSongs.put(songs) } } @@ -127,8 +133,10 @@ constructor( */ fun renamePlaylist(playlist: Playlist, name: String? = null) { if (name != null) { + logD("Renaming $playlist to $name") viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) } } else { + logD("Launching rename dialog for $playlist") _playlistToRename.put(playlist) } } @@ -142,8 +150,10 @@ constructor( */ fun deletePlaylist(playlist: Playlist, rude: Boolean = false) { if (rude) { + logD("Deleting $playlist") viewModelScope.launch(Dispatchers.IO) { musicRepository.deletePlaylist(playlist) } } else { + logD("Launching deletion dialog for $playlist") _playlistToDelete.put(playlist) } } @@ -155,6 +165,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(song: Song, playlist: Playlist? = null) { + logD("Adding $song to playlist") addToPlaylist(listOf(song), playlist) } @@ -165,6 +176,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(album: Album, playlist: Playlist? = null) { + logD("Adding $album to playlist") addToPlaylist(musicSettings.albumSongSort.songs(album.songs), playlist) } @@ -175,6 +187,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(artist: Artist, playlist: Playlist? = null) { + logD("Adding $artist to playlist") addToPlaylist(musicSettings.artistSongSort.songs(artist.songs), playlist) } @@ -185,6 +198,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(genre: Genre, playlist: Playlist? = null) { + logD("Adding $genre to playlist") addToPlaylist(musicSettings.genreSongSort.songs(genre.songs), playlist) } @@ -196,8 +210,10 @@ constructor( */ fun addToPlaylist(songs: List, playlist: Playlist? = null) { if (playlist != null) { + logD("Adding ${songs.size} songs to $playlist") viewModelScope.launch(Dispatchers.IO) { musicRepository.addToPlaylist(songs, playlist) } } else { + logD("Launching addition dialog for songs=${songs.size}") _songsToAdd.put(songs) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt index 1e227e53c..0b91c65b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.cache import javax.inject.Inject import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE /** @@ -49,7 +50,9 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached try { // Faster to load the whole database into memory than do a query on each // populate call. - CacheImpl(cachedSongsDao.readSongs()) + val songs = cachedSongsDao.readSongs() + logD("Successfully read ${songs.size} songs from cache") + CacheImpl(songs) } catch (e: Exception) { logE("Unable to load cache database.") logE(e.stackTraceToString()) @@ -60,7 +63,9 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached try { // Still write out whatever data was extracted. cachedSongsDao.nukeSongs() + logD("Successfully deleted old cache") cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw)) + logD("Successfully wrote ${rawSongs.size} songs to cache") } catch (e: Exception) { logE("Unable to save cache database.") logE(e.stackTraceToString()) @@ -96,7 +101,6 @@ private class CacheImpl(cachedSongs: List) : Cache { override var invalidated = false override fun populate(rawSong: RawSong): Boolean { - // For a cached raw song to be used, it must exist within the cache and have matching // addition and modification timestamps. Technically the addition timestamp doesn't // exist, but to safeguard against possible OEM-specific timestamp incoherence, we diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index a4631a9ea..66d1c5d77 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -149,6 +149,9 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings return hashCode } + override fun toString() = + "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, artists=${artists.size}, genres=${genres.size})" + override fun findSong(uid: Music.UID) = songUidMap[uid] override fun findAlbum(uid: Music.UID) = albumUidMap[uid] override fun findArtist(uid: Music.UID) = artistUidMap[uid] diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 47328689f..c07c5a65f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -96,6 +96,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son override fun hashCode() = 31 * uid.hashCode() + rawSong.hashCode() override fun equals(other: Any?) = other is SongImpl && uid == other.uid && rawSong == other.rawSong + override fun toString() = "Song(uid=$uid, name=$name)" private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings) private val artistNames = rawSong.artistNames.parseMultiValue(musicSettings) @@ -262,6 +263,8 @@ class AlbumImpl( override fun equals(other: Any?) = other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs + override fun toString() = "Album(uid=$uid, name=$name)" + private val _artists = mutableListOf() override val artists: List get() = _artists @@ -363,6 +366,8 @@ class ArtistImpl( rawArtist == other.rawArtist && songs == other.songs + override fun toString() = "Artist(uid=$uid, name=$name)" + override lateinit var genres: List init { @@ -449,6 +454,8 @@ class GenreImpl( override fun equals(other: Any?) = other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs + override fun toString() = "Genre(uid=$uid, name=$name)" + init { val distinctAlbums = mutableSetOf() val distinctArtists = mutableSetOf() diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt index 5913c2b8c..5e0799d72 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt @@ -25,6 +25,7 @@ import org.oxycblt.auxio.databinding.ItemMusicDirBinding import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater +import org.oxycblt.auxio.util.logD /** * [RecyclerView.Adapter] that manages a list of [Directory] instances. @@ -54,10 +55,8 @@ class DirectoryAdapter(private val listener: Listener) : * @param dir The [Directory] to add. */ fun add(dir: Directory) { - if (_dirs.contains(dir)) { - return - } - + if (_dirs.contains(dir)) return + logD("Adding $dir") _dirs.add(dir) notifyItemInserted(_dirs.lastIndex) } @@ -65,9 +64,10 @@ class DirectoryAdapter(private val listener: Listener) : /** * Add a list of [Directory] instances to the end of the list. * - * @param dirs The [Directory instances to add. + * @param dirs The [Directory] instances to add. */ fun addAll(dirs: List) { + logD("Adding ${dirs.size} directories") val oldLastIndex = dirs.lastIndex _dirs.addAll(dirs) notifyItemRangeInserted(oldLastIndex, dirs.size) @@ -79,6 +79,7 @@ class DirectoryAdapter(private val listener: Listener) : * @param dir The [Directory] to remove. Must exist in the list. */ fun remove(dir: Directory) { + logD("Removing $dir") val idx = _dirs.indexOf(dir) _dirs.removeAt(idx) notifyItemRemoved(idx) @@ -86,6 +87,7 @@ class DirectoryAdapter(private val listener: Listener) : /** A Listener for [DirectoryAdapter] interactions. */ interface Listener { + /** Called when the delete button on a directory item is clicked. */ fun onRemoveDirectory(dir: Directory) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt index 63ef57a0c..93a777a6a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt @@ -145,18 +145,10 @@ data class MusicDirectories(val dirs: List, val shouldInclude: Boolea * @param fromFormat The mime type obtained by analyzing the file format. Null if could not be * obtained. * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Get around to simplifying this */ data class MimeType(val fromExtension: String, val fromFormat: String?) { - - /** - * Return a mime-type such as "audio/ogg" - * - * @return A raw mime-type string. Will first try [fromFormat], then falling back to - * [fromExtension], and then null if that fails. - */ - val raw: String - get() = fromFormat ?: fromExtension - /** * Resolve the mime type into a human-readable format name, such as "Ogg Vorbis". * diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index 0df80983b..4e8faae19 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -120,6 +120,7 @@ private abstract class BaseMediaStoreExtractor( if (dirs.dirs.isNotEmpty()) { selector += " AND " if (!dirs.shouldInclude) { + logD("Excluding directories in selector") // Without a NOT, the query will be restricted to the specified paths, resulting // in the "Include" mode. With a NOT, the specified paths will not be included, // resulting in the "Exclude" mode. @@ -144,14 +145,14 @@ private abstract class BaseMediaStoreExtractor( } // Now we can actually query MediaStore. - logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]") + logD("Starting song query [proj=${projection.toList()}, selector=$selector, args=$args]") val cursor = context.contentResolverSafe.safeQuery( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selector, args.toTypedArray()) - logD("Song query succeeded [Projected total: ${cursor.count}]") + logD("Successfully queried for ${cursor.count} songs") val genreNamesMap = mutableMapOf() @@ -185,6 +186,7 @@ private abstract class BaseMediaStoreExtractor( } } } + logD("Read ${genreNamesMap.size} genres from MediaStore") logD("Finished initialization in ${System.currentTimeMillis() - start}ms") return wrapQuery(cursor, genreNamesMap) diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt index 46e4130f0..1d717ad43 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt @@ -24,6 +24,7 @@ import java.text.SimpleDateFormat import kotlin.math.max import org.oxycblt.auxio.R import org.oxycblt.auxio.util.inRangeOrNull +import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.nonZeroOrNull /** @@ -51,27 +52,25 @@ class Date private constructor(private val tokens: List) : Comparable * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will * be properly localized. */ - fun resolveDate(context: Context): String { - if (month != null) { - // Parse a date format from an ISO-ish format - val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat) - format.applyPattern("yyyy-MM") - val date = - try { - format.parse("$year-$month") - } catch (e: ParseException) { - null - } - - if (date != null) { - // Reformat as a readable month and year - format.applyPattern("MMM yyyy") - return format.format(date) - } - } - + fun resolve(context: Context) = // Unable to create fine-grained date, just format as a year. - return context.getString(R.string.fmt_number, year) + month?.let { resolveFineGrained() } ?: context.getString(R.string.fmt_number, year) + + private fun resolveFineGrained(): String? { + // We can't directly load a date with our own + val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat) + format.applyPattern("yyyy-MM") + val date = + try { + format.parse("$year-$month") + } catch (e: ParseException) { + logE("Unable to parse fine-grained date: $e") + return null + } + + // Reformat as a readable month and year + format.applyPattern("MMM yyyy") + return format.format(date) } override fun hashCode() = tokens.hashCode() @@ -139,9 +138,9 @@ class Date private constructor(private val tokens: List) : Comparable fun resolveDate(context: Context) = if (min != max) { context.getString( - R.string.fmt_date_range, min.resolveDate(context), max.resolveDate(context)) + R.string.fmt_date_range, min.resolve(context), max.resolve(context)) } else { - min.resolveDate(context) + min.resolve(context) } override fun equals(other: Any?) = other is Range && min == other.min && max == other.max diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt index 759d52b49..52b7ab646 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt @@ -27,6 +27,7 @@ import org.oxycblt.auxio.list.Item * @param name The name of the disc group, if any. Null if not present. */ class Disc(val number: Int, val name: String?) : Item, Comparable { + // We don't want to group discs by differing subtitles, so only compare by the number override fun equals(other: Any?) = other is Disc && number == other.number override fun hashCode() = number.hashCode() override fun compareTo(other: Disc) = number.compareTo(other.number) diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt index 3b7c3bfc7..6b508f56a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -201,6 +201,7 @@ private data class IntelligentKnownName(override val raw: String, override val s // Separate each token into their numeric and lexicographic counterparts. if (token.first().isDigit()) { // The digit string comparison breaks with preceding zero digits, remove those + // TODO: Handle zero digits in other languages val digits = token.trimStart('0').ifEmpty { token } // Other languages have other types of digit strings, still use collation keys collationKey = COLLATOR.getCollationKey(digits) diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt index acea28744..6e3646b62 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt @@ -104,25 +104,23 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. null } - val resolvedMimeType = - if (song.mimeType.fromFormat != null) { - // ExoPlayer was already able to populate the format. - song.mimeType - } else { - // ExoPlayer couldn't populate the format somehow, populate it here. - val formatMimeType = - try { - format.getString(MediaFormat.KEY_MIME) - } catch (e: NullPointerException) { - logE("Unable to extract mime type field") - null - } - - MimeType(song.mimeType.fromExtension, formatMimeType) + // The song's mime type won't have a populated format field right now, try to + // extract it ourselves. + val formatMimeType = + try { + format.getString(MediaFormat.KEY_MIME) + } catch (e: NullPointerException) { + logE("Unable to extract mime type field") + null } extractor.release() - return AudioProperties(bitrate, sampleRate, resolvedMimeType) + logD("Finished extracting audio properties") + + return AudioProperties( + bitrate, + sampleRate, + MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt index 5fa612667..3496ea059 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt @@ -30,12 +30,15 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.ui.ViewBindingDialogFragment +import org.oxycblt.auxio.util.logW /** * A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to * split tags with multiple values. * * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Replace with unsplit names dialog */ @AndroidEntryPoint class SeparatorsDialog : ViewBindingDialogFragment() { @@ -74,7 +77,7 @@ class SeparatorsDialog : ViewBindingDialogFragment() { Separators.SLASH -> binding.separatorSlash.isChecked = true Separators.PLUS -> binding.separatorPlus.isChecked = true Separators.AND -> binding.separatorAnd.isChecked = true - else -> error("Unexpected separator in settings data") + else -> logW("Unexpected separator in settings data") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt index bbc2971a0..4cca1a824 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt @@ -23,6 +23,7 @@ import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.yield import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.util.logD /** * The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the @@ -52,6 +53,8 @@ class TagExtractorImpl @Inject constructor(private val tagWorkerFactory: TagWork // producing similar throughput's to other kinds of manual metadata extraction. val tagWorkerPool: Array = arrayOfNulls(TASK_CAPACITY) + logD("Beginning primary extraction loop") + for (incompleteRawSong in incompleteSongs) { spin@ while (true) { for (i in tagWorkerPool.indices) { @@ -71,6 +74,8 @@ class TagExtractorImpl @Inject constructor(private val tagWorkerFactory: TagWork } } + logD("All incomplete songs exhausted, starting cleanup loop") + do { var ongoingTasks = false for (i in tagWorkerPool.indices) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt index 115462a8a..b709cb558 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -89,12 +89,8 @@ private class TagWorkerImpl( } catch (e: Exception) { logW("Unable to extract metadata for ${rawSong.name}") logW(e.stackTraceToString()) - null + return rawSong } - if (format == null) { - logD("Nothing could be extracted for ${rawSong.name}") - return rawSong - } val metadata = format.metadata if (metadata != null) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt index 1cdb8b4db..d22f9a5e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt @@ -35,6 +35,7 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast /** @@ -93,7 +94,7 @@ class AddToPlaylistDialog : private fun updatePendingSongs(songs: List?) { if (songs == null) { - // No songs to feasibly add to a playlist, leave. + logD("No songs to show choices for, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt index afc90c825..15d347199 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt @@ -32,6 +32,7 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull @@ -76,7 +77,7 @@ class DeletePlaylistDialog : ViewBindingDialogFragment() private fun updatePendingPlaylist(pendingPlaylist: PendingPlaylist?) { if (pendingPlaylist == null) { + logD("No playlist to create, leaving") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt index 2181f4605..51a9895cb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt @@ -31,6 +31,9 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logE +import org.oxycblt.auxio.util.logW /** * A [ViewModel] managing the state of the playlist picker dialogs. @@ -84,6 +87,8 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M pendingPlaylist.preferredName, pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) }) } + logD("Updated pending playlist: ${_currentPendingPlaylist.value?.preferredName}") + _currentSongsToAdd.value = _currentSongsToAdd.value?.let { pendingSongs -> pendingSongs @@ -91,6 +96,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M .ifEmpty { null } .also { refreshChoicesWith = it } } + logD("Updated songs to add: ${_currentSongsToAdd.value?.size} songs") } val chosenName = _chosenName.value @@ -102,6 +108,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M // Nothing to do. } } + logD("Updated chosen name to $chosenName") refreshChoicesWith = refreshChoicesWith ?: _currentSongsToAdd.value } @@ -119,19 +126,34 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param songUids The [Music.UID]s of songs to be present in the playlist. */ fun setPendingPlaylist(context: Context, songUids: Array) { - val deviceLibrary = musicRepository.deviceLibrary ?: return - val songs = songUids.mapNotNull(deviceLibrary::findSong) - + logD("Opening ${songUids.size} songs to create a playlist from") val userLibrary = musicRepository.userLibrary ?: return - var i = 1 - while (true) { - val possibleName = context.getString(R.string.fmt_def_playlist, i) - if (userLibrary.playlists.none { it.name.resolve(context) == possibleName }) { - _currentPendingPlaylist.value = PendingPlaylist(possibleName, songs) - return + val songs = + musicRepository.deviceLibrary + ?.let { songUids.mapNotNull(it::findSong) } + ?.also(::refreshPlaylistChoices) + + val possibleName = + musicRepository.userLibrary?.let { + // Attempt to generate a unique default name for the playlist, like "Playlist 1". + var i = 1 + var possibleName: String + do { + possibleName = context.getString(R.string.fmt_def_playlist, i) + logD("Trying $possibleName as a playlist name") + ++i + } while (userLibrary.playlists.any { it.name.resolve(context) == possibleName }) + logD("$possibleName is unique, using it as the playlist name") + possibleName + } + + _currentPendingPlaylist.value = + if (possibleName != null && songs != null) { + PendingPlaylist(possibleName, songs) + } else { + logW("Given song UIDs to create were invalid") + null } - ++i - } } /** @@ -140,7 +162,11 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param playlistUid The [Music.UID]s of the [Playlist] to rename. */ fun setPlaylistToRename(playlistUid: Music.UID) { + logD("Opening playlist $playlistUid to rename") _currentPlaylistToRename.value = musicRepository.userLibrary?.findPlaylist(playlistUid) + if (_currentPlaylistToDelete.value == null) { + logW("Given playlist UID to rename was invalid") + } } /** @@ -149,7 +175,11 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param playlistUid The [Music.UID] of the [Playlist] to delete. */ fun setPlaylistToDelete(playlistUid: Music.UID) { + logD("Opening playlist $playlistUid to delete") _currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid) + if (_currentPlaylistToDelete.value == null) { + logW("Given playlist UID to delete was invalid") + } } /** @@ -158,16 +188,25 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param name The new user-inputted name, or null if not present. */ fun updateChosenName(name: String?) { + logD("Updating chosen name to $name") _chosenName.value = when { - name.isNullOrEmpty() -> ChosenName.Empty - name.isBlank() -> ChosenName.Blank + name.isNullOrEmpty() -> { + logE("Chosen name is empty") + ChosenName.Empty + } + name.isBlank() -> { + logE("Chosen name is blank") + ChosenName.Blank + } else -> { val trimmed = name.trim() val userLibrary = musicRepository.userLibrary if (userLibrary != null && userLibrary.findPlaylist(trimmed) == null) { + logD("Chosen name is valid") ChosenName.Valid(trimmed) } else { + logD("Chosen name already exists in library") ChosenName.AlreadyExists(trimmed) } } @@ -180,14 +219,19 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param songUids The [Music.UID]s of songs to add to a playlist. */ fun setSongsToAdd(songUids: Array) { - val deviceLibrary = musicRepository.deviceLibrary ?: return - val songs = songUids.mapNotNull(deviceLibrary::findSong) - _currentSongsToAdd.value = songs - refreshPlaylistChoices(songs) + logD("Opening ${songUids.size} songs to add to a playlist") + _currentSongsToAdd.value = + musicRepository.deviceLibrary + ?.let { songUids.mapNotNull(it::findSong).ifEmpty { null } } + ?.also(::refreshPlaylistChoices) + if (_currentSongsToAdd.value == null || songUids.size != _currentSongsToAdd.value?.size) { + logW("Given song UIDs to add were (partially) invalid") + } } private fun refreshPlaylistChoices(songs: List) { val userLibrary = musicRepository.userLibrary ?: return + logD("Refreshing playlist choices") _playlistAddChoices.value = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map { val songSet = it.songs.toSet() diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/RenamePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/RenamePlaylistDialog.kt index fcc8b2538..20ed39bd5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/RenamePlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/RenamePlaylistDialog.kt @@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull @@ -86,7 +87,9 @@ class RenamePlaylistDialog : ViewBindingDialogFragment): MutableUserLibrary { // While were waiting for the library, read our playlists out. - val rawPlaylists = playlistDao.readRawPlaylists() + val rawPlaylists = + try { + playlistDao.readRawPlaylists() + } catch (e: Exception) { + logE("Unable to read playlists: $e") + return UserLibraryImpl(playlistDao, mutableMapOf(), musicSettings) + } + logD("Successfully read ${rawPlaylists.size} playlists") val deviceLibrary = deviceLibraryChannel.receive() // Convert the database playlist information to actual usable playlists. val playlistMap = mutableMapOf() @@ -139,6 +149,8 @@ private class UserLibraryImpl( private val playlistMap: MutableMap, private val musicSettings: MusicSettings ) : MutableUserLibrary { + override fun toString() = "UserLibrary(playlists=${playlists.size})" + override val playlists: List get() = playlistMap.values.toList() @@ -153,34 +165,74 @@ private class UserLibraryImpl( RawPlaylist( PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw), playlistImpl.songs.map { PlaylistSong(it.uid) }) - playlistDao.insertPlaylist(rawPlaylist) + try { + playlistDao.insertPlaylist(rawPlaylist) + logD("Successfully created playlist $name with ${songs.size} songs") + } catch (e: Exception) { + logE("Unable to create playlist $name with ${songs.size} songs") + logE(e.stackTraceToString()) + synchronized(this) { playlistMap.remove(playlistImpl.uid) } + return + } } override suspend fun renamePlaylist(playlist: Playlist, name: String) { val playlistImpl = requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" } synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(name, musicSettings) } - playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name)) + try { + playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name)) + logD("Successfully renamed $playlist to $name") + } catch (e: Exception) { + logE("Unable to rename $playlist to $name: $e") + logE(e.stackTraceToString()) + synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } + return + } } override suspend fun deletePlaylist(playlist: Playlist) { - synchronized(this) { - requireNotNull(playlistMap.remove(playlist.uid)) { "Cannot remove invalid playlist" } + val playlistImpl = + requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" } + synchronized(this) { playlistMap.remove(playlistImpl.uid) } + try { + playlistDao.deletePlaylist(playlist.uid) + logD("Successfully deleted $playlist") + } catch (e: Exception) { + logE("Unable to delete $playlist: $e") + logE(e.stackTraceToString()) + synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } + return } - playlistDao.deletePlaylist(playlist.uid) } override suspend fun addToPlaylist(playlist: Playlist, songs: List) { val playlistImpl = requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" } synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) } } - playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) + try { + playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) + logD("Successfully added ${songs.size} songs to $playlist") + } catch (e: Exception) { + logE("Unable to add ${songs.size} songs to $playlist: $e") + logE(e.stackTraceToString()) + synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } + return + } } override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { val playlistImpl = requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" } synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(songs) } - playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) + try { + playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) + logD("Successfully rewrote $playlist with ${songs.size} songs") + } catch (e: Exception) { + logE("Unable to rewrite $playlist with ${songs.size} songs: $e") + logE(e.stackTraceToString()) + synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } + return + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt index 6e2f43f83..5271e49c7 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt @@ -32,7 +32,7 @@ import org.oxycblt.auxio.util.logD * * @author Alexander Capehart (OxygenCobalt) * - * TODO: This whole system is very jankily designed, perhaps it's time for a refactor? + * TODO: Unwind this into ViewModel-specific actions, and then reference those. */ class NavigationViewModel : ViewModel() { private val _mainNavigationAction = MutableEvent() @@ -96,6 +96,7 @@ class NavigationViewModel : ViewModel() { * dialog will be shown. */ fun exploreNavigateToParentArtist(song: Song) { + logD("Navigating to parent artist of $song") exploreNavigateToParentArtistImpl(song, song.artists) } @@ -106,6 +107,7 @@ class NavigationViewModel : ViewModel() { * dialog will be shown. */ fun exploreNavigateToParentArtist(album: Album) { + logD("Navigating to parent artist of $album") exploreNavigateToParentArtistImpl(album, album.artists) } diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt index a8614af77..ade74f930 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt @@ -78,7 +78,7 @@ class NavigateToArtistDialog : override fun onDestroyBinding(binding: DialogMusicChoicesBinding) { super.onDestroyBinding(binding) - choiceAdapter + binding.choiceRecycler.adapter = null } override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) { diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt index f7f011ca3..f02621d5b 100644 --- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt @@ -28,6 +28,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.util.logD /** * A [ViewModel] that stores the current information required for navigation picker dialogs @@ -62,6 +63,7 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository: } else -> null } + logD("Updated artist choices: ${_artistChoices.value}") } override fun onCleared() { @@ -75,12 +77,22 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository: * @param itemUid The [Music.UID] of the item to show. Must be a [Song] or [Album]. */ fun setArtistChoiceUid(itemUid: Music.UID) { + logD("Opening navigation choices for $itemUid") // Support Songs and Albums, which have parent artists. _artistChoices.value = when (val music = musicRepository.find(itemUid)) { - is Song -> SongArtistNavigationChoices(music) - is Album -> AlbumArtistNavigationChoices(music) - else -> null + is Song -> { + logD("Creating navigation choices for song") + SongArtistNavigationChoices(music) + } + is Album -> { + logD("Creating navigation choices for album") + AlbumArtistNavigationChoices(music) + } + else -> { + logD("Given song/album UID was invalid") + null + } } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index e5be7d6ca..05f438c38 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -34,6 +34,7 @@ import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getColorCompat +import org.oxycblt.auxio.util.logD /** * A [ViewBindingFragment] that shows the current playback state in a compact manner. @@ -93,6 +94,7 @@ class PlaybackBarFragment : ViewBindingFragment() { private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, actionMode: ActionMode) { when (actionMode) { ActionMode.NEXT -> { + logD("Setting up skip next action") binding.playbackSecondaryAction.apply { setIconResource(R.drawable.ic_skip_next_24) contentDescription = getString(R.string.desc_skip_next) @@ -101,6 +103,7 @@ class PlaybackBarFragment : ViewBindingFragment() { } } ActionMode.REPEAT -> { + logD("Setting up repeat mode action") binding.playbackSecondaryAction.apply { contentDescription = getString(R.string.desc_change_repeat) iconTint = context.getColorCompat(R.color.sel_activatable_icon) @@ -109,6 +112,7 @@ class PlaybackBarFragment : ViewBindingFragment() { } } ActionMode.SHUFFLE -> { + logD("Setting up shuffle action") binding.playbackSecondaryAction.apply { setIconResource(R.drawable.sel_shuffle_state_24) contentDescription = getString(R.string.desc_shuffle) @@ -121,14 +125,17 @@ class PlaybackBarFragment : ViewBindingFragment() { } private fun updateSong(song: Song?) { - if (song != null) { - val context = requireContext() - val binding = requireBinding() - binding.playbackCover.bind(song) - binding.playbackSong.text = song.name.resolve(context) - binding.playbackInfo.text = song.artists.resolveNames(context) - binding.playbackProgressBar.max = song.durationMs.msToDs().toInt() + if (song == null) { + // Nothing to do. + return } + + val context = requireContext() + val binding = requireBinding() + binding.playbackCover.bind(song) + binding.playbackSong.text = song.name.resolve(context) + binding.playbackInfo.text = song.artists.resolveNames(context) + binding.playbackProgressBar.max = song.durationMs.msToDs().toInt() } private fun updatePlaying(isPlaying: Boolean) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 2cce12949..abd7aafbc 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -43,6 +43,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.systemBarInsetsCompat @@ -142,6 +143,7 @@ class PlaybackPanelFragment : when (item.itemId) { R.id.action_open_equalizer -> { // Launch the system equalizer app, if possible. + logD("Launching equalizer") val equalizerIntent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) // Provide audio session ID so the equalizer can show options for this app @@ -200,6 +202,7 @@ class PlaybackPanelFragment : val binding = requireBinding() val context = requireContext() + logD("Updating song display: $song") binding.playbackCover.bind(song) binding.playbackSong.text = song.name.resolve(context) binding.playbackArtist.text = song.artists.resolveNames(context) 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 2a65c85a3..8ec5db941 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt @@ -198,8 +198,14 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont when (key) { getString(R.string.set_key_replay_gain), getString(R.string.set_key_pre_amp_with), - getString(R.string.set_key_pre_amp_without) -> listener.onReplayGainSettingsChanged() - getString(R.string.set_key_notif_action) -> listener.onNotificationActionChanged() + getString(R.string.set_key_pre_amp_without) -> { + logD("Dispatching ReplayGain setting change") + listener.onReplayGainSettingsChanged() + } + getString(R.string.set_key_notif_action) -> { + logD("Dispatching notification setting change") + listener.onNotificationActionChanged() + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 89ca23c43..028ef516e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -43,6 +43,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent +import org.oxycblt.auxio.util.logD /** * An [ViewModel] that provides a safe UI frontend for the current playback state. @@ -124,27 +125,32 @@ constructor( } override fun onIndexMoved(queue: Queue) { + logD("Index moved, updating current song") _song.value = queue.currentSong } override fun onQueueChanged(queue: Queue, change: Queue.Change) { // Other types of queue changes preserve the current song. if (change.type == Queue.Change.Type.SONG) { + logD("Queue changed, updating current song") _song.value = queue.currentSong } } override fun onQueueReordered(queue: Queue) { + logD("Queue completely changed, updating current song") _isShuffled.value = queue.isShuffled } override fun onNewPlayback(queue: Queue, parent: MusicParent?) { + logD("New playback started, updating playback information") _song.value = queue.currentSong _parent.value = parent _isShuffled.value = queue.isShuffled } override fun onStateChanged(state: InternalPlayer.State) { + logD("Player state changed, starting new position polling") _isPlaying.value = state.isPlaying // Still need to update the position now due to co-routine launch delays _positionDs.value = state.calculateElapsedPositionMs().msToDs() @@ -169,6 +175,7 @@ constructor( /** Shuffle all songs in the music library. */ fun shuffleAll() { + logD("Shuffling all songs") playImpl(null, null, true) } @@ -184,6 +191,7 @@ constructor( * @param playbackMode The [MusicMode] to play from. */ fun playFrom(song: Song, playbackMode: MusicMode) { + logD("Playing $song from $playbackMode") when (playbackMode) { MusicMode.SONGS -> playImpl(song, null) MusicMode.ALBUMS -> playImpl(song, song.album) @@ -202,10 +210,13 @@ constructor( */ fun playFromArtist(song: Song, artist: Artist? = null) { if (artist != null) { + logD("Playing $song from $artist") playImpl(song, artist) } else if (song.artists.size == 1) { + logD("$song has one artist, playing from it") playImpl(song, song.artists[0]) } else { + logD("$song has multiple artists, showing choice dialog") _artistPlaybackPickerSong.put(song) } } @@ -219,10 +230,13 @@ constructor( */ fun playFromGenre(song: Song, genre: Genre? = null) { if (genre != null) { + logD("Playing $song from $genre") playImpl(song, genre) } else if (song.genres.size == 1) { + logD("$song has one genre, playing from it") playImpl(song, song.genres[0]) } else { + logD("$song has multiple genres, showing choice dialog") _genrePlaybackPickerSong.put(song) } } @@ -234,6 +248,7 @@ constructor( * @param playlist The [Playlist] to play from. Must be linked to the [Song]. */ fun playFromPlaylist(song: Song, playlist: Playlist) { + logD("Playing $song from $playlist") playImpl(song, playlist) } @@ -242,70 +257,100 @@ constructor( * * @param album The [Album] to play. */ - fun play(album: Album) = playImpl(null, album, false) + fun play(album: Album) { + logD("Playing $album") + playImpl(null, album, false) + } /** * Play an [Artist]. * * @param artist The [Artist] to play. */ - fun play(artist: Artist) = playImpl(null, artist, false) + fun play(artist: Artist) { + logD("Playing $artist") + playImpl(null, artist, false) + } /** * Play a [Genre]. * * @param genre The [Genre] to play. */ - fun play(genre: Genre) = playImpl(null, genre, false) + fun play(genre: Genre) { + logD("Playing $genre") + playImpl(null, genre, false) + } /** * Play a [Playlist]. * * @param playlist The [Playlist] to play. */ - fun play(playlist: Playlist) = playImpl(null, playlist, false) + fun play(playlist: Playlist) { + logD("Playing $playlist") + playImpl(null, playlist, false) + } /** * Play a list of [Song]s. * * @param songs The [Song]s to play. */ - fun play(songs: List) = playbackManager.play(null, null, songs, false) + fun play(songs: List) { + logD("Playing ${songs.size} songs") + playbackManager.play(null, null, songs, false) + } /** * Shuffle an [Album]. * * @param album The [Album] to shuffle. */ - fun shuffle(album: Album) = playImpl(null, album, true) + fun shuffle(album: Album) { + logD("Shuffling $album") + playImpl(null, album, true) + } /** * Shuffle an [Artist]. * * @param artist The [Artist] to shuffle. */ - fun shuffle(artist: Artist) = playImpl(null, artist, true) + fun shuffle(artist: Artist) { + logD("Shuffling $artist") + playImpl(null, artist, true) + } /** * Shuffle a [Genre]. * * @param genre The [Genre] to shuffle. */ - fun shuffle(genre: Genre) = playImpl(null, genre, true) + fun shuffle(genre: Genre) { + logD("Shuffling $genre") + playImpl(null, genre, true) + } /** * Shuffle a [Playlist]. * * @param playlist The [Playlist] to shuffle. */ - fun shuffle(playlist: Playlist) = playImpl(null, playlist, true) + fun shuffle(playlist: Playlist) { + logD("Shuffling $playlist") + playImpl(null, playlist, true) + } /** * Shuffle a list of [Song]s. * * @param songs The [Song]s to shuffle. */ - fun shuffle(songs: List) = playbackManager.play(null, null, songs, true) + fun shuffle(songs: List) { + logD("Shuffling ${songs.size} songs") + playbackManager.play(null, null, songs, true) + } private fun playImpl( song: Song?, @@ -334,6 +379,7 @@ constructor( * @param action The [InternalPlayer.Action] to perform eventually. */ fun startAction(action: InternalPlayer.Action) { + logD("Starting action $action") playbackManager.startAction(action) } @@ -345,6 +391,7 @@ constructor( * @param positionDs The position to seek to, in deci-seconds (1/10th of a second). */ fun seekTo(positionDs: Long) { + logD("Seeking to ${positionDs}ds") playbackManager.seekTo(positionDs.dsToMs()) } @@ -352,11 +399,13 @@ constructor( /** Skip to the next [Song]. */ fun next() { + logD("Skipping to next song") playbackManager.next() } /** Skip to the previous [Song]. */ fun prev() { + logD("Skipping to previous song") playbackManager.prev() } @@ -366,6 +415,7 @@ constructor( * @param song The [Song] to add. */ fun playNext(song: Song) { + logD("Playing $song next") playbackManager.playNext(song) } @@ -375,6 +425,7 @@ constructor( * @param album The [Album] to add. */ fun playNext(album: Album) { + logD("Playing $album next") playbackManager.playNext(musicSettings.albumSongSort.songs(album.songs)) } @@ -384,6 +435,7 @@ constructor( * @param artist The [Artist] to add. */ fun playNext(artist: Artist) { + logD("Playing $artist next") playbackManager.playNext(musicSettings.artistSongSort.songs(artist.songs)) } @@ -393,6 +445,7 @@ constructor( * @param genre The [Genre] to add. */ fun playNext(genre: Genre) { + logD("Playing $genre next") playbackManager.playNext(musicSettings.genreSongSort.songs(genre.songs)) } @@ -402,6 +455,7 @@ constructor( * @param playlist The [Playlist] to add. */ fun playNext(playlist: Playlist) { + logD("Playing $playlist next") playbackManager.playNext(playlist.songs) } @@ -411,6 +465,7 @@ constructor( * @param songs The [Song]s to add. */ fun playNext(songs: List) { + logD("Playing ${songs.size} songs next") playbackManager.playNext(songs) } @@ -420,6 +475,7 @@ constructor( * @param song The [Song] to add. */ fun addToQueue(song: Song) { + logD("Adding $song to queue") playbackManager.addToQueue(song) } @@ -429,6 +485,7 @@ constructor( * @param album The [Album] to add. */ fun addToQueue(album: Album) { + logD("Adding $album to queue") playbackManager.addToQueue(musicSettings.albumSongSort.songs(album.songs)) } @@ -438,6 +495,7 @@ constructor( * @param artist The [Artist] to add. */ fun addToQueue(artist: Artist) { + logD("Adding $artist to queue") playbackManager.addToQueue(musicSettings.artistSongSort.songs(artist.songs)) } @@ -447,6 +505,7 @@ constructor( * @param genre The [Genre] to add. */ fun addToQueue(genre: Genre) { + logD("Adding $genre to queue") playbackManager.addToQueue(musicSettings.genreSongSort.songs(genre.songs)) } @@ -456,6 +515,7 @@ constructor( * @param playlist The [Playlist] to add. */ fun addToQueue(playlist: Playlist) { + logD("Adding $playlist to queue") playbackManager.addToQueue(playlist.songs) } @@ -465,6 +525,7 @@ constructor( * @param songs The [Song]s to add. */ fun addToQueue(songs: List) { + logD("Adding ${songs.size} songs to queue") playbackManager.addToQueue(songs) } @@ -472,11 +533,13 @@ constructor( /** Toggle [isPlaying] (i.e from playing to paused) */ fun togglePlaying() { + logD("Toggling playing state") playbackManager.setPlaying(!playbackManager.playerState.isPlaying) } /** Toggle [isShuffled] (ex. from on to off) */ fun toggleShuffled() { + logD("Toggling shuffled state") playbackManager.reorder(!playbackManager.queue.isShuffled) } @@ -486,6 +549,7 @@ constructor( * @see RepeatMode.increment */ fun toggleRepeatMode() { + logD("Toggling repeat mode") playbackManager.repeatMode = playbackManager.repeatMode.increment() } @@ -497,6 +561,7 @@ constructor( * @param onDone Called when the save is completed with true if successful, and false otherwise. */ fun savePlaybackState(onDone: (Boolean) -> Unit) { + logD("Saving playback state") viewModelScope.launch { onDone(persistenceRepository.saveState(playbackManager.toSavedState())) } @@ -508,6 +573,7 @@ constructor( * @param onDone Called when the wipe is completed with true if successful, and false otherwise. */ fun wipePlaybackState(onDone: (Boolean) -> Unit) { + logD("Wiping playback state") viewModelScope.launch { onDone(persistenceRepository.saveState(null)) } } @@ -518,6 +584,7 @@ constructor( * otherwise. */ fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) { + logD("Force-restoring playback state") viewModelScope.launch { val savedState = persistenceRepository.readState() if (savedState != null) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt index a246689fe..00b1a8894 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt @@ -61,7 +61,7 @@ constructor( heap = queueDao.getHeap() mapping = queueDao.getMapping() } catch (e: Exception) { - logE("Unable to load playback state data") + logE("Unable read playback state") logE(e.stackTraceToString()) return null } @@ -74,7 +74,7 @@ constructor( } val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent } - logD("Read playback state") + logD("Successfully read playback state") return PlaybackStateManager.SavedState( parent = parent, @@ -90,8 +90,6 @@ constructor( } override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean { - // Only bother saving a state if a song is actively playing from one. - // This is not the case with a null state. try { playbackStateDao.nukeState() queueDao.nukeHeap() @@ -101,7 +99,8 @@ constructor( logE(e.stackTraceToString()) return false } - logD("Cleared state") + + logD("Successfully cleared previous state") if (state != null) { // Transform saved state into raw state, which can then be written to the database. val playbackState = @@ -118,12 +117,14 @@ constructor( state.queueState.heap.mapIndexed { i, song -> QueueHeapItem(i, requireNotNull(song).uid) } + val mapping = state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed { i, pair -> QueueMappingItem(i, pair.first, pair.second) } + try { playbackStateDao.insertState(playbackState) queueDao.insertHeap(heap) @@ -133,8 +134,10 @@ constructor( logE(e.stackTraceToString()) return false } - logD("Wrote state") + + logD("Successfully wrote new state") } + return true } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt index 0d477bd8d..c8fc134e7 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt @@ -35,6 +35,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -72,6 +73,7 @@ class PlayFromArtistDialog : if (it != null) { choiceAdapter.update(it.artists, UpdateInstructions.Replace(0)) } else { + logD("No song to show choices for, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt index 0b8914dc2..1f2693a10 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt @@ -35,6 +35,7 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -72,6 +73,7 @@ class PlayFromGenreDialog : if (it != null) { choiceAdapter.update(it.genres, UpdateInstructions.Replace(0)) } else { + logD("No song to show choices for, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt index 313e1b4d4..644b5a580 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt @@ -27,6 +27,8 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW /** * A [ViewModel] that stores the choices shown in the playback picker dialogs. @@ -62,6 +64,10 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M * @param uid The [Music.UID] of the item to show. Must be a [Song]. */ fun setPickerSongUid(uid: Music.UID) { + logD("Opening picker for song $uid") _currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid) + if (_currentPickerSong.value != null) { + logW("Given song UID was invalid") + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt index 3a54bc1c6..ed864e675 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt @@ -23,8 +23,7 @@ import kotlin.random.nextInt import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.playback.queue.Queue.Change.Type -import org.oxycblt.auxio.playback.queue.Queue.SavedState +import org.oxycblt.auxio.util.logD /** * A heap-backed play queue. @@ -176,6 +175,8 @@ class EditableQueue : Queue { return } + logD("Reordering queue [shuffled=$shuffled]") + if (shuffled) { val trueIndex = if (shuffledMapping.isNotEmpty()) { @@ -192,7 +193,7 @@ class EditableQueue : Queue { shuffledMapping.add(0, shuffledMapping.removeAt(shuffledMapping.indexOf(trueIndex))) index = 0 } else if (shuffledMapping.isNotEmpty()) { - // Un-shuffling, song to preserve is in the shuffled mapping. + // Ordering queue, song to preserve is in the shuffled mapping. index = orderedMapping.indexOf(shuffledMapping[index]) shuffledMapping = mutableListOf() } @@ -206,15 +207,18 @@ class EditableQueue : Queue { * @return A [Queue.Change] instance that reflects the changes made. */ fun playNext(songs: List): Queue.Change { + logD("Adding ${songs.size} songs to the front of the queue") val heapIndices = songs.map(::addSongToHeap) if (shuffledMapping.isNotEmpty()) { // Add the new songs in front of the current index in the shuffled mapping and in front // of the analogous list song in the ordered mapping. + logD("Must append songs to shuffled mapping") val orderedIndex = orderedMapping.indexOf(shuffledMapping[index]) orderedMapping.addAll(orderedIndex + 1, heapIndices) shuffledMapping.addAll(index + 1, heapIndices) } else { // Add the new song in front of the current index in the ordered mapping. + logD("Only appending songs to ordered mapping") orderedMapping.addAll(index + 1, heapIndices) } check() @@ -229,10 +233,12 @@ class EditableQueue : Queue { * @return A [Queue.Change] instance that reflects the changes made. */ fun addToQueue(songs: List): Queue.Change { + logD("Adding ${songs.size} songs to the back of the queue") val heapIndices = songs.map(::addSongToHeap) // Can simple append the new songs to the end of both mappings. orderedMapping.addAll(heapIndices) if (shuffledMapping.isNotEmpty()) { + logD("Appending songs to shuffled mapping") shuffledMapping.addAll(heapIndices) } check() @@ -257,19 +263,33 @@ class EditableQueue : Queue { orderedMapping.add(dst, orderedMapping.removeAt(src)) } + val oldIndex = index when (index) { // We are moving the currently playing song, correct the index to it's new position. - src -> index = dst + src -> { + logD("Moving current song, shifting index") + index = dst + } // We have moved an song from behind the playing song to in front, shift back. - in (src + 1)..dst -> index -= 1 + in (src + 1)..dst -> { + logD("Moving song from behind -> front, shift backwards") + index -= 1 + } // We have moved an song from in front of the playing song to behind, shift forward. - in dst until src -> index += 1 + in dst until src -> { + logD("Moving song from front -> behind, shift forward") + index += 1 + } else -> { // Nothing to do. + logD("Move preserved index") check() return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Move(src, dst)) } } + + logD("Move changed index: $oldIndex -> $index") + check() return Queue.Change(Queue.Change.Type.INDEX, UpdateInstructions.Move(src, dst)) } @@ -298,15 +318,23 @@ class EditableQueue : Queue { val type = when { // We just removed the currently playing song. - index == at -> Queue.Change.Type.SONG + index == at -> { + logD("Removed current song") + Queue.Change.Type.SONG + } // Index was ahead of removed song, shift back to preserve consistency. index > at -> { + logD("Removed before current song, shift back") index -= 1 Queue.Change.Type.INDEX } // Nothing to do - else -> Queue.Change.Type.MAPPING + else -> { + logD("Removal preserved index") + Queue.Change.Type.MAPPING + } } + logD("Committing change of type $type") check() return Queue.Change(type, UpdateInstructions.Remove(at, 1)) } @@ -339,6 +367,8 @@ class EditableQueue : Queue { } } + logD("Serialized heap [max shift=$currentShift]") + heap = savedState.heap.filterNotNull().toMutableList() orderedMapping = savedState.orderedMapping.mapNotNullTo(mutableListOf()) { heapIndex -> @@ -354,6 +384,7 @@ class EditableQueue : Queue { while (currentSong?.uid != savedState.songUid && index > -1) { index-- } + logD("Corrected index: ${savedState.index} -> $index") check() } @@ -373,13 +404,17 @@ class EditableQueue : Queue { orphanCandidates.add(entry.index) } } + logD("Found orphans: ${orphanCandidates.map { heap[it] }}") orphanCandidates.removeAll(currentMapping.toSet()) if (orphanCandidates.isNotEmpty()) { + val orphan = orphanCandidates.first() + logD("Found an orphan that could be re-used: ${heap[orphan]}") // There are orphaned songs, return the first one we find. - return orphanCandidates.first() + return orphan } } // Nothing to re-use, add this song to the queue + logD("No orphan could be re-used") heap.add(song) return heap.lastIndex } 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 c9425bb82..501b58af8 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 @@ -88,9 +88,13 @@ class QueueAdapter(private val listener: EditClickListListener) : // Have to update not only the currently playing item, but also all items marked // as playing. + // TODO: Optimize this by only updating the range between old and new indices? + // TODO: Don't update when the index has not moved. if (currentIndex < lastIndex) { + logD("Moved backwards, must update items above last index") notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_POSITION) } else { + logD("Moved forwards, update items after index") notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_POSITION) } @@ -121,6 +125,10 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS alpha = 0 } + /** + * Whether this ViewHolder should be full-opacity to represent a future item, or greyed out to + * represent a past item. True if former, false if latter. + */ var isFuture: Boolean get() = binding.songAlbumCover.isEnabled set(value) { 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 414ab0eeb..8e75abf36 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,6 +33,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD /** * A [ViewBindingFragment] that displays an editable queue. @@ -122,13 +123,15 @@ class QueueFragment : ViewBindingFragment(), EditClickList // dependent on where we have to scroll to get to the currently playing song. if (notInitialized || scrollTo < start) { // We need to scroll upwards, or initialize the scroll, no need to offset + logD("Not scrolling downwards, no offset needed") binding.queueRecycler.scrollToPosition(scrollTo) } else if (scrollTo > end) { // We need to scroll downwards, we need to offset by a screen of songs. // This does have some error due to how many completely visible items on-screen // can vary. This is considered okay. - binding.queueRecycler.scrollToPosition( - min(queue.lastIndex, scrollTo + (end - start))) + val offset = scrollTo + (end - start) + logD("Scrolling downwards, offsetting by $offset") + binding.queueRecycler.scrollToPosition(min(queue.lastIndex, offset)) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 13099ef7b..5b1edce73 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent +import org.oxycblt.auxio.util.logD /** * A [ViewModel] that manages the current queue state and allows navigation through the queue. @@ -60,22 +61,26 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt } override fun onIndexMoved(queue: Queue) { + logD("Index moved, synchronizing and scrolling to new position") _scrollTo.put(queue.index) _index.value = queue.index } override fun onQueueChanged(queue: Queue, change: Queue.Change) { // Queue changed trivially due to item mo -> Diff queue, stay at current index. + logD("Updating queue display") _queueInstructions.put(change.instructions) _queue.value = queue.resolve() if (change.type != Queue.Change.Type.MAPPING) { // Index changed, make sure it remains updated without actually scrolling to it. + logD("Index changed with queue, synchronizing new position") _index.value = queue.index } } override fun onQueueReordered(queue: Queue) { // Queue changed completely -> Replace queue, update index + logD("Queue changed completely, replacing queue and position") _queueInstructions.put(UpdateInstructions.Replace(0)) _scrollTo.put(queue.index) _queue.value = queue.resolve() @@ -84,6 +89,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt override fun onNewPlayback(queue: Queue, parent: MusicParent?) { // Entirely new queue -> Replace queue, update index + logD("New playback, replacing queue and position") _queueInstructions.put(UpdateInstructions.Replace(0)) _scrollTo.put(queue.index) _queue.value = queue.resolve() @@ -102,6 +108,10 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt * range. */ fun goto(adapterIndex: Int) { + if (adapterIndex !in queue.value.indices) { + return + } + logD("Going to position $adapterIndex in queue") playbackManager.goto(adapterIndex) } @@ -115,6 +125,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt if (adapterIndex !in queue.value.indices) { return } + logD("Removing item $adapterIndex in queue") playbackManager.removeQueueItem(adapterIndex) } @@ -129,6 +140,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt if (adapterFrom !in queue.value.indices || adapterTo !in queue.value.indices) { return false } + logD("Moving $adapterFrom to $adapterFrom in queue") playbackManager.moveQueueItem(adapterFrom, adapterTo) return true } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt index 07815dde4..dcd7db42e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.ui.ViewBindingDialogFragment +import org.oxycblt.auxio.util.logD /** * aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp]. @@ -61,6 +62,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { // settings. After this, the sliders save their own state, so we do not need to // do any restore behavior. val preAmp = playbackSettings.replayGainPreAmp + logD("Initializing from $preAmp") binding.withTagsSlider.value = preAmp.with binding.withoutTagsSlider.value = preAmp.without } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index 7bb57376c..ab86651e0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -125,14 +125,22 @@ constructor( when (playbackSettings.replayGainMode) { // User wants track gain to be preferred. Default to album gain only if // there is no track gain. - ReplayGainMode.TRACK -> gain.track == 0f + ReplayGainMode.TRACK -> { + logD("Using track strategy") + gain.track == 0f + } // User wants album gain to be preferred. Default to track gain only if // here is no album gain. - ReplayGainMode.ALBUM -> gain.album != 0f + ReplayGainMode.ALBUM -> { + logD("Using album strategy") + gain.album != 0f + } // User wants album gain to be used when in an album, track gain otherwise. - ReplayGainMode.DYNAMIC -> + ReplayGainMode.DYNAMIC -> { + logD("Using dynamic strategy") playbackManager.parent is Album && playbackManager.queue.currentSong?.album == playbackManager.parent + } } val resolvedGain = @@ -184,6 +192,7 @@ constructor( textTags.vorbis[TAG_RG_TRACK_GAIN] ?.run { first().parseReplayGainAdjustment() } ?.let { albumGain = it } + // Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the // adjustment by 256 to get the gain. This is used alongside the base adjustment // intrinsic to the format to create the normalized adjustment. This is normally the only diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 6562db8cf..501cf03d9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -24,7 +24,6 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.queue.EditableQueue import org.oxycblt.auxio.playback.queue.Queue -import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -308,8 +307,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { override val queue = EditableQueue() @Volatile - override var parent: MusicParent? = - null // FIXME: Parent is interpreted wrong when nothing is playing. + override var parent: MusicParent? = null private set @Volatile override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0) @@ -373,6 +371,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun play(song: Song?, parent: MusicParent?, queue: List, shuffled: Boolean) { val internalPlayer = internalPlayer ?: return + logD("Playing $song from $parent in ${queue.size}-song queue [shuffled=$shuffled]") // Set up parent and queue this.parent = parent this.queue.start(song, queue, shuffled) @@ -392,6 +391,9 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { if (!queue.goto(queue.index + 1)) { queue.goto(0) play = repeatMode == RepeatMode.ALL + logD("At end of queue, wrapping around to position 0 [play=$play]") + } else { + logD("Moving to next song") } notifyIndexMoved() internalPlayer.loadSong(queue.currentSong, play) @@ -400,12 +402,13 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun prev() { val internalPlayer = internalPlayer ?: return - // If enabled, rewind before skipping back if the position is past 3 seconds [3000ms] if (internalPlayer.shouldRewindWithPrev) { + logD("Rewinding current song") rewind() setPlaying(true) } else { + logD("Moving to previous song") if (!queue.goto(queue.index - 1)) { queue.goto(0) } @@ -418,16 +421,21 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { override fun goto(index: Int) { val internalPlayer = internalPlayer ?: return if (queue.goto(index)) { + logD("Moving to $index") notifyIndexMoved() internalPlayer.loadSong(queue.currentSong, true) + } else { + logW("$index was not in bounds, could not move to it") } } @Synchronized override fun playNext(songs: List) { if (queue.currentSong == null) { + logD("Nothing playing, short-circuiting to new playback") play(songs[0], null, songs, false) } else { + logD("Adding ${songs.size} songs to start of queue") notifyQueueChanged(queue.playNext(songs)) } } @@ -435,8 +443,10 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun addToQueue(songs: List) { if (queue.currentSong == null) { + logD("Nothing playing, short-circuiting to new playback") play(songs[0], null, songs, false) } else { + logD("Adding ${songs.size} songs to end of queue") notifyQueueChanged(queue.addToQueue(songs)) } } @@ -460,6 +470,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun reorder(shuffled: Boolean) { + logD("Reordering queue [shuffled=$shuffled]") queue.reorder(shuffled) notifyQueueReordered() } @@ -504,11 +515,13 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun setPlaying(isPlaying: Boolean) { + logD("Updating playing state to $isPlaying") internalPlayer?.setPlaying(isPlaying) } @Synchronized override fun seekTo(positionMs: Long) { + logD("Seeking to ${positionMs}ms") internalPlayer?.seekTo(positionMs) } @@ -530,10 +543,11 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { destructive: Boolean ) { if (isInitialized && !destructive) { + logW("Already initialized, cannot apply saved state") return } val internalPlayer = internalPlayer ?: return - logD("Restoring state $savedState") + logD("Applying state $savedState") val lastSong = queue.currentSong parent = savedState.parent @@ -545,10 +559,12 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { // it be. Specifically done so we don't pause on music updates that don't really change // what's playing (ex. playlist editing) if (lastSong != queue.currentSong) { + logD("Song changed, must reload player") // Continuing playback while also possibly doing drastic state updates is // a bad idea, so pause. internalPlayer.loadSong(queue.currentSong, false) if (queue.currentSong != null) { + logD("Seeking to saved position ${savedState.positionMs}ms") // Internal player may have reloaded the media item, re-seek to the previous // position seekTo(savedState.positionMs) @@ -560,36 +576,42 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { // --- CALLBACKS --- private fun notifyIndexMoved() { + logD("Dispatching index change") for (callback in listeners) { callback.onIndexMoved(queue) } } private fun notifyQueueChanged(change: Queue.Change) { + logD("Dispatching queue change $change") for (callback in listeners) { callback.onQueueChanged(queue, change) } } private fun notifyQueueReordered() { + logD("Dispatching queue reordering") for (callback in listeners) { callback.onQueueReordered(queue) } } private fun notifyNewPlayback() { + logD("Dispatching new playback") for (callback in listeners) { callback.onNewPlayback(queue, parent) } } private fun notifyStateChanged() { + logD("Dispatching player state change") for (callback in listeners) { callback.onStateChanged(playerState) } } private fun notifyRepeatModeChanged() { + logD("Dispatching repeat mode change") for (callback in listeners) { callback.onRepeatChanged(repeatMode) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt index 6c19d42b6..b875636b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt @@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.playback.state.PlaybackStateManager +import org.oxycblt.auxio.util.logD /** * A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to [PlaybackService]. @@ -43,6 +44,7 @@ class MediaButtonReceiver : BroadcastReceiver() { // stupid this is with the state of foreground services on modern android. One // wrong action at the wrong time will result in the app crashing, and there is // nothing I can do about it. + logD("Delivering media button intent $intent") intent.component = ComponentName(context, PlaybackService::class.java) ContextCompat.startForegroundService(context, intent) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 1d44ebc46..9d273ac98 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -86,6 +86,7 @@ constructor( * @param intent The [Intent.ACTION_MEDIA_BUTTON] [Intent] to forward. */ fun handleMediaButtonIntent(intent: Intent) { + logD("Forwarding $intent to MediaButtonReciever") MediaButtonReceiver.handleIntent(mediaSession, intent) } @@ -283,8 +284,10 @@ constructor( * playback is currently occuring from all songs. */ private fun updateMediaMetadata(song: Song?, parent: MusicParent?) { + logD("Updating media metadata to $song with $parent") if (song == null) { // Nothing playing, reset the MediaSession and close the notification. + logD("Nothing playing, resetting media session") mediaSession.setMetadata(emptyMetadata) return } @@ -316,12 +319,17 @@ constructor( .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs) // These fields are nullable and so we must check first before adding them to the fields. song.track?.let { + logD("Adding track information") builder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, it.toLong()) } song.disc?.let { + logD("Adding disc information") builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.number.toLong()) } - song.date?.let { builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString()) } + song.date?.let { + logD("Adding date information") + builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString()) + } // We are normally supposed to use URIs for album art, but that removes some of the // nice things we can do like square cropping or high quality covers. Instead, @@ -330,6 +338,8 @@ constructor( song, object : BitmapProvider.Target { override fun onCompleted(bitmap: Bitmap?) { + this@MediaSessionComponent.logD( + "Bitmap loaded, applying media " + "session and posting notification") builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap) builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) val metadata = builder.build() @@ -364,6 +374,7 @@ constructor( // playback state. MediaSessionCompat.QueueItem(description, i.toLong()) } + logD("Uploading ${queueItems.size} songs to MediaSession queue") mediaSession.setQueue(queueItems) } @@ -384,7 +395,8 @@ constructor( // Add the secondary action (either repeat/shuffle depending on the configuration) val secondaryAction = when (playbackSettings.notificationAction) { - ActionMode.SHUFFLE -> + ActionMode.SHUFFLE -> { + logD("Using shuffle MediaSession action") PlaybackStateCompat.CustomAction.Builder( PlaybackService.ACTION_INVERT_SHUFFLE, context.getString(R.string.desc_shuffle), @@ -393,11 +405,14 @@ constructor( } else { R.drawable.ic_shuffle_off_24 }) - else -> + } + else -> { + logD("Using repeat mode MediaSession action") PlaybackStateCompat.CustomAction.Builder( PlaybackService.ACTION_INC_REPEAT_MODE, context.getString(R.string.desc_change_repeat), playbackManager.repeatMode.icon) + } } state.addCustomAction(secondaryAction.build()) @@ -415,14 +430,22 @@ constructor( /** Invalidate the "secondary" action (i.e shuffle/repeat mode). */ private fun invalidateSecondaryAction() { + logD("Invalidating secondary action") invalidateSessionState() when (playbackSettings.notificationAction) { - ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.queue.isShuffled) - else -> notification.updateRepeatMode(playbackManager.repeatMode) + ActionMode.SHUFFLE -> { + logD("Using shuffle notification action") + notification.updateShuffled(playbackManager.queue.isShuffled) + } + else -> { + logD("Using repeat mode notification action") + notification.updateRepeatMode(playbackManager.repeatMode) + } } if (!bitmapProvider.isBusy) { + logD("Not loading a bitmap, post the notification") listener?.onPostNotification(notification) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt index a6410a274..ecbbce122 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt @@ -31,6 +31,7 @@ import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.service.ForegroundServiceNotification +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newBroadcastPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent @@ -73,6 +74,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes * @param metadata The [MediaMetadataCompat] to display in this notification. */ fun updateMetadata(metadata: MediaMetadataCompat) { + logD("Updating shown metadata") setLargeIcon(metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)) setContentTitle(metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)) setContentText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ARTIST)) @@ -81,8 +83,10 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes // content text to being above the title. Use an appropriate field for both. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // Display description -> Parent in which playback is occurring + logD("API 24+, showing parent information") setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION)) } else { + logD("API 24 or lower, showing album information") setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ALBUM)) } } @@ -93,6 +97,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes * @param isPlaying Whether playback should be indicated as ongoing or paused. */ fun updatePlaying(isPlaying: Boolean) { + logD("Updating playing state: $isPlaying") mActions[2] = buildPlayPauseAction(context, isPlaying) } @@ -102,6 +107,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes * @param repeatMode The current [RepeatMode]. */ fun updateRepeatMode(repeatMode: RepeatMode) { + logD("Applying repeat mode action: $repeatMode") mActions[0] = buildRepeatAction(context, repeatMode) } @@ -111,6 +117,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes * @param isShuffled Whether the queue is currently shuffled or not. */ fun updateShuffled(isShuffled: Boolean) { + logD("Applying shuffle action: $isShuffled") mActions[0] = buildShuffleAction(context, isShuffled) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index cfee787cc..e08cd7f2e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -56,6 +56,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetProvider @@ -243,6 +244,7 @@ class PlaybackService : } override fun setPlaying(isPlaying: Boolean) { + logD("Updating player state to $isPlaying") player.playWhenReady = isPlaying } @@ -254,14 +256,17 @@ class PlaybackService : if (player.playWhenReady) { // Mark that we have started playing so that the notification can now be posted. hasPlayed = true + logD("Player has started playing") if (!openAudioEffectSession) { // Convention to start an audioeffect session on play/pause rather than // start/stop + logD("Opening audio effect session") broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) openAudioEffectSession = true } } else if (openAudioEffectSession) { // Make sure to close the audio session when we stop playback. + logD("Closing audio effect session") broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) openAudioEffectSession = false } @@ -273,6 +278,7 @@ class PlaybackService : Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_POSITION_DISCONTINUITY)) { + logD("Player state changed, must synchronize state") playbackManager.synchronizeState(this) } } @@ -281,12 +287,15 @@ class PlaybackService : if (state == Player.STATE_ENDED) { // Player ended, repeat the current track if we are configured to. if (playbackManager.repeatMode == RepeatMode.TRACK) { + logD("Looping current track") playbackManager.rewind() // May be configured to pause when we repeat a track. if (playbackSettings.pauseOnRepeat) { + logD("Pausing track on loop") playbackManager.setPlaying(false) } } else { + logD("Track ended, moving to next track") playbackManager.next() } } @@ -295,12 +304,15 @@ class PlaybackService : override fun onPlayerError(error: PlaybackException) { // TODO: Replace with no skipping and a notification instead // If there's any issue, just go to the next song. + logE("Player error occured") + logE(error.stackTraceToString()) playbackManager.next() } override fun onMusicChanges(changes: MusicRepository.Changes) { if (changes.deviceLibrary && musicRepository.deviceLibrary != null) { // We now have a library, see if we have anything we need to do. + logD("Library obtained, requesting action") playbackManager.requestAction(this) } } @@ -308,6 +320,7 @@ class PlaybackService : // --- OTHER FUNCTIONS --- private fun broadcastAudioEffectAction(event: String) { + logD("Broadcasting AudioEffect event: $event") sendBroadcast( Intent(event) .putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName) @@ -333,11 +346,10 @@ class PlaybackService : // No library, cannot do anything. ?: return false - logD("Performing action: $action") - when (action) { // Restore state -> Start a new restoreState job is InternalPlayer.Action.RestoreState -> { + logD("Restoring playback state") restoreScope.launch { persistenceRepository.readState()?.let { playbackManager.applySavedState(it, false) @@ -346,11 +358,13 @@ class PlaybackService : } // Shuffle all -> Start new playback from all songs is InternalPlayer.Action.ShuffleAll -> { + logD("Shuffling all tracks") playbackManager.play( null, null, musicSettings.songSort.songs(deviceLibrary.songs), true) } // Open -> Try to find the Song for the given file and then play it from all songs is InternalPlayer.Action.Open -> { + logD("Opening specified file") deviceLibrary.findSongForUri(application, action.uri)?.let { song -> playbackManager.play( song, @@ -371,8 +385,9 @@ class PlaybackService : // where changing a setting would cause the notification to appear in an unfriendly // manner. if (hasPlayed) { - logD("Updating notification") + logD("Played before, starting foreground state") if (!foregroundManager.tryStartForeground(notification)) { + logD("Notification changed, re-posting") notification.post() } } @@ -397,6 +412,7 @@ class PlaybackService : // 3. Some internal framework thing that also handles bluetooth headsets // Just use ACTION_HEADSET_PLUG. AudioManager.ACTION_HEADSET_PLUG -> { + logD("Received headset plug event") when (intent.getIntExtra("state", -1)) { 0 -> pauseFromHeadsetPlug() 1 -> playFromHeadsetPlug() @@ -404,21 +420,41 @@ class PlaybackService : initialHeadsetPlugEventHandled = true } - AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromHeadsetPlug() + AudioManager.ACTION_AUDIO_BECOMING_NOISY -> { + logD("Received Headset noise event") + pauseFromHeadsetPlug() + } // --- AUXIO EVENTS --- - ACTION_PLAY_PAUSE -> + ACTION_PLAY_PAUSE -> { + logD("Received play event") playbackManager.setPlaying(!playbackManager.playerState.isPlaying) - ACTION_INC_REPEAT_MODE -> + } + ACTION_INC_REPEAT_MODE -> { + logD("Received repeat mode event") playbackManager.repeatMode = playbackManager.repeatMode.increment() - ACTION_INVERT_SHUFFLE -> playbackManager.reorder(!playbackManager.queue.isShuffled) - ACTION_SKIP_PREV -> playbackManager.prev() - ACTION_SKIP_NEXT -> playbackManager.next() + } + ACTION_INVERT_SHUFFLE -> { + logD("Received shuffle event") + playbackManager.reorder(!playbackManager.queue.isShuffled) + } + ACTION_SKIP_PREV -> { + logD("Received skip previous event") + playbackManager.prev() + } + ACTION_SKIP_NEXT -> { + logD("Received skip next event") + playbackManager.next() + } ACTION_EXIT -> { + logD("Received exit event") playbackManager.setPlaying(false) stopAndSave() } - WidgetProvider.ACTION_WIDGET_UPDATE -> widgetComponent.update() + WidgetProvider.ACTION_WIDGET_UPDATE -> { + logD("Received widget update event") + widgetComponent.update() + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index 7023e9361..f4d787704 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -25,6 +25,7 @@ import com.google.android.material.button.MaterialButton import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.RippleFixMaterialButton import org.oxycblt.auxio.util.getInteger +import org.oxycblt.auxio.util.logD /** * A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when @@ -46,10 +47,12 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 val targetRadius = if (activated) 0.3f else 0.5f if (!isLaidOut) { // Not laid out, initialize it without animation before drawing. + logD("Not laid out, immediately updating corner radius") updateCornerRadiusRatio(targetRadius) return } + logD("Starting corner radius animation") animator?.cancel() animator = ValueAnimator.ofFloat(currentCornerRadiusRatio, targetRadius).apply { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt index e37cdf660..20bbd9394 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt @@ -81,6 +81,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 // zero, use 1 instead and disable the SeekBar. val to = max(value, 1) isEnabled = value > 0 + logD("Value sanitization finished [to=$to, enabled=$isEnabled]") // Sanity check 2: If the current value exceeds the new duration value, clamp it // down so that we don't crash and instead have an annoying visual flicker. if (positionDs > to) { diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt index ee83b5418..51acee744 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Name +import org.oxycblt.auxio.util.logD /** * Implements the fuzzy-ish searching algorithm used in the search view. @@ -65,8 +66,9 @@ interface SearchEngine { class SearchEngineImpl @Inject constructor(@ApplicationContext private val context: Context) : SearchEngine { - override suspend fun search(items: SearchEngine.Items, query: String) = - SearchEngine.Items( + override suspend fun search(items: SearchEngine.Items, query: String): SearchEngine.Items { + logD("Launching search for $query") + return SearchEngine.Items( songs = items.songs?.searchListImpl(query) { q, song -> song.path.name.contains(q, ignoreCase = true) @@ -75,6 +77,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte artists = items.artists?.searchListImpl(query), genres = items.genres?.searchListImpl(query), playlists = items.playlists?.searchListImpl(query)) + } /** * Search a given [Music] list. 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 a79371305..0839d12fb 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -115,6 +115,7 @@ class SearchFragment : ListFragment() { if (!launchedKeyboard) { // Auto-open the keyboard when this view is shown + this@SearchFragment.logD("Keyboard is not shown yet") showKeyboard(this) launchedKeyboard = true } @@ -155,6 +156,7 @@ class SearchFragment : ListFragment() { if (item.itemId != R.id.submenu_filtering) { // Is a change in filter mode and not just a junk submenu click, update // the filtering within SearchViewModel. + logD("Filter mode selected") item.isChecked = true searchModel.setFilterOptionId(item.itemId) return true @@ -189,6 +191,7 @@ class SearchFragment : ListFragment() { // 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. + logD("Update finished, scrolling to top") binding.searchRecycler.scrollToPosition(0) } } @@ -233,6 +236,7 @@ class SearchFragment : ListFragment() { * @param view The [View] to focus the keyboard on. */ private fun showKeyboard(view: View) { + logD("Launching keyboard") view.apply { requestFocus() postDelayed(200) { @@ -244,6 +248,7 @@ class SearchFragment : ListFragment() { /** Safely hide the keyboard from this view. */ private fun hideKeyboard() { + logD("Hiding keyboard") requireNotNull(imm) { "InputMethodManager was not available" } .hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index db1cc3c09..8a3aa5a1c 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -78,6 +78,7 @@ constructor( override fun onMusicChanges(changes: MusicRepository.Changes) { if (changes.deviceLibrary || changes.userLibrary) { + logD("Music changed, re-searching library") search(lastQuery) } } @@ -96,14 +97,13 @@ constructor( val deviceLibrary = musicRepository.deviceLibrary val userLibrary = musicRepository.userLibrary if (query.isNullOrEmpty() || deviceLibrary == null || userLibrary == null) { - logD("Search query is not applicable.") + logD("Cannot search for the current query, aborting") _searchResults.value = listOf() return } - logD("Searching music library for $query") - // Searching is time-consuming, so do it in the background. + logD("Searching music library for $query") currentSearchJob = viewModelScope.launch { _searchResults.value = @@ -121,6 +121,7 @@ constructor( val items = if (filterMode == null) { // A nulled filter mode means to not filter anything. + logD("No filter mode specified, using entire library") SearchEngine.Items( deviceLibrary.songs, deviceLibrary.albums, @@ -128,6 +129,7 @@ constructor( deviceLibrary.genres, userLibrary.playlists) } else { + logD("Filter mode specified, filtering library") SearchEngine.Items( songs = if (filterMode == MusicMode.SONGS) deviceLibrary.songs else null, albums = if (filterMode == MusicMode.ALBUMS) deviceLibrary.albums else null, @@ -141,11 +143,13 @@ constructor( return buildList { results.artists?.let { + logD("Adding ${it.size} artists to search results") val header = BasicHeader(R.string.lbl_artists) add(header) addAll(SORT.artists(it)) } results.albums?.let { + logD("Adding ${it.size} albums to search results") val header = BasicHeader(R.string.lbl_albums) if (isNotEmpty()) { add(Divider(header)) @@ -155,6 +159,7 @@ constructor( addAll(SORT.albums(it)) } results.playlists?.let { + logD("Adding ${it.size} playlists to search results") val header = BasicHeader(R.string.lbl_playlists) if (isNotEmpty()) { add(Divider(header)) @@ -164,6 +169,7 @@ constructor( addAll(SORT.playlists(it)) } results.genres?.let { + logD("Adding ${it.size} genres to search results") val header = BasicHeader(R.string.lbl_genres) if (isNotEmpty()) { add(Divider(header)) @@ -173,6 +179,7 @@ constructor( addAll(SORT.genres(it)) } results.songs?.let { + logD("Adding ${it.size} songs to search results") val header = BasicHeader(R.string.lbl_songs) if (isNotEmpty()) { add(Divider(header)) 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 0bda7bd9d..8288d7443 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -108,6 +108,7 @@ class AboutFragment : ViewBindingFragment() { // Android 11 seems to now handle the app chooser situations on its own now // [along with adding a new permission that breaks the old manual code], so // we just do a typical activity launch. + logD("Using API 30+ chooser") try { context.startActivity(browserIntent) } catch (e: ActivityNotFoundException) { @@ -119,6 +120,7 @@ class AboutFragment : ViewBindingFragment() { // not work in all cases, especially when no default app was set. If that is the // case, we will try to manually handle these cases before we try to launch the // browser. + logD("Resolving browser activity for chooser") @Suppress("DEPRECATION") val pkgName = context.packageManager @@ -128,16 +130,17 @@ class AboutFragment : ViewBindingFragment() { if (pkgName != null) { if (pkgName == "android") { // No default browser [Must open app chooser, may not be supported] + logD("No default browser found") 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 logD("Opening browser intent") + 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) @@ -151,6 +154,7 @@ class AboutFragment : ViewBindingFragment() { * @param intent The [Intent] to show an app chooser for. */ private fun openAppChooser(intent: Intent) { + logD("Opening app chooser for ${intent.action}") val chooserIntent = Intent(Intent.ACTION_CHOOSER) .putExtra(Intent.EXTRA_INTENT, intent) diff --git a/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt index f739f6b29..14065ea47 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt @@ -107,9 +107,10 @@ abstract class BasePreferenceFragment(@XmlRes private val screen: Int) : when (preference) { is IntListPreference -> { // Copy the built-in preference dialog launching code into our project so - // we can automatically use the provided preference class. + // we can automatically use the provided preference class. The deprecated code + // is largely unavoidable. val dialog = IntListPreferenceDialog.from(preference) - dialog.setTargetFragment(this, 0) + @Suppress("DEPRECATION") dialog.setTargetFragment(this, 0) dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG) } is WrappedDialogPreference -> { @@ -128,6 +129,7 @@ abstract class BasePreferenceFragment(@XmlRes private val screen: Int) : } if (preference is PreferenceCategory) { + // Recurse into preference children to make sure they are set up as well preference.children.forEach(::setupPreference) return } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt index 0467abcda..fe2bf0066 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt @@ -30,6 +30,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.settings.ui.WrappedDialogPreference +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast @@ -64,18 +65,22 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) { // do one. when (preference.key) { getString(R.string.set_key_ui) -> { + logD("Navigating to UI preferences") findNavController() .navigateSafe(RootPreferenceFragmentDirections.goToUiPreferences()) } getString(R.string.set_key_personalize) -> { + logD("Navigating to personalization preferences") findNavController() .navigateSafe(RootPreferenceFragmentDirections.goToPersonalizePreferences()) } getString(R.string.set_key_music) -> { + logD("Navigating to music preferences") findNavController() .navigateSafe(RootPreferenceFragmentDirections.goToMusicPreferences()) } getString(R.string.set_key_audio) -> { + logD("Navigating to audio preferences") findNavController() .navigateSafe(RootPreferenceFragmentDirections.goToAudioPreferences()) } @@ -85,6 +90,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) { playbackModel.savePlaybackState { saved -> // Use the nullable context, as we could try to show a toast when this // fragment is no longer attached. + logD("Showing saving confirmation") if (saved) { context?.showToast(R.string.lbl_state_saved) } else { @@ -94,6 +100,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) { } getString(R.string.set_key_wipe_state) -> { playbackModel.wipePlaybackState { wiped -> + logD("Showing wipe confirmation") if (wiped) { // Use the nullable context, as we could try to show a toast when this // fragment is no longer attached. @@ -105,6 +112,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) { } getString(R.string.set_key_restore_state) -> playbackModel.tryRestorePlaybackState { restored -> + logD("Showing restore confirmation") if (restored) { // Use the nullable context, as we could try to show a toast when this // fragment is no longer attached. diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt index 49eda0656..5bcee531f 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt @@ -22,6 +22,7 @@ import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe /** @@ -33,6 +34,7 @@ class AudioPreferenceFragment : BasePreferenceFragment(R.xml.preferences_audio) override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_pre_amp)) { + logD("Navigating to pre-amp dialog") findNavController().navigateSafe(AudioPreferenceFragmentDirections.goToPreAmpDialog()) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt index 8e60a1b33..b0b59d02b 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt @@ -26,6 +26,7 @@ import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe /** @@ -39,6 +40,7 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_separators)) { + logD("Navigating to separator dialog") findNavController() .navigateSafe(MusicPreferenceFragmentDirections.goToSeparatorsDialog()) } @@ -46,8 +48,10 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) override fun onSetupPreference(preference: Preference) { if (preference.key == getString(R.string.set_key_cover_mode)) { + logD("Configuring cover mode setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> + logD("Cover mode changed, resetting image memory cache") imageLoader.memoryCache?.clear() true } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt index 8669c52c3..f284e1d69 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt @@ -22,6 +22,7 @@ import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe /** @@ -32,6 +33,7 @@ import org.oxycblt.auxio.util.navigateSafe class PersonalizePreferenceFragment : BasePreferenceFragment(R.xml.preferences_personalize) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_home_tabs)) { + logD("Navigating to home tab dialog") findNavController() .navigateSafe(PersonalizePreferenceFragmentDirections.goToTabDialog()) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt index b1105f123..8d7ba5114 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt @@ -28,6 +28,7 @@ import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.isNight +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe /** @@ -41,6 +42,7 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_accent)) { + logD("Navigating to accent dialog") findNavController().navigateSafe(UIPreferenceFragmentDirections.goToAccentDialog()) } } @@ -48,20 +50,25 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) { override fun onSetupPreference(preference: Preference) { when (preference.key) { getString(R.string.set_key_theme) -> { + logD("Configuring theme setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value -> + logD("Theme changed, recreating") AppCompatDelegate.setDefaultNightMode(value as Int) true } } getString(R.string.set_key_accent) -> { + logD("Configuring accent setting") preference.summary = getString(uiSettings.accent.name) } getString(R.string.set_key_black_theme) -> { + logD("Configuring black theme setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> val activity = requireActivity() if (activity.isNight) { + logD("Black theme changed in night mode, recreating") activity.recreate() } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt index d0cafd9f0..9937d1eac 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt @@ -28,6 +28,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import org.oxycblt.auxio.R import org.oxycblt.auxio.util.getDimen +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.systemGestureInsetsCompat /** @@ -82,6 +83,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: val layout = super.onLayoutChild(parent, child, layoutDirection) // Don't repeat redundant initialization. if (!initalized) { + logD("Not initialized, setting up child") child.apply { // Set up compat elevation attributes. These are only shown below API 28. translationZ = context.getDimen(R.dimen.elevation_normal) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt index eb3560271..c8fc0305c 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt @@ -26,6 +26,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import kotlin.math.abs import org.oxycblt.auxio.util.coordinatorLayoutBehavior +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat @@ -60,10 +61,12 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri val behavior = dependency.coordinatorLayoutBehavior as BackportBottomSheetBehavior val consumed = behavior.calculateConsumedByBar() if (consumed == Int.MIN_VALUE) { + logD("Not laid out yet, cannot update dependent view") return false } if (consumed != lastConsumed) { + logD("Consumed amount changed, re-applying insets") lastConsumed = consumed val insets = lastInsets diff --git a/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt index 002f49868..afa60e6fd 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt @@ -30,6 +30,7 @@ import androidx.core.content.res.ResourcesCompat import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout import org.oxycblt.auxio.util.coordinatorLayoutBehavior +import org.oxycblt.auxio.util.logD /** * An [AppBarLayout] that resolves two issues with the default implementation: @@ -75,6 +76,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr fun expandWithScrollingRecycler() { setExpanded(true) (findScrollingChild() as? RecyclerView)?.let { + logD("Found RecyclerView, expanding with it") addOnOffsetChangedListener(ExpansionHackListener(it)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt index 657b5c6ca..137e9abe9 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt @@ -51,6 +51,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr fun setVisible(@IdRes viewId: Int): Boolean { val index = children.indexOfFirst { it.id == viewId } if (index == currentlyVisible) return false + logD("Switching toolbar visibility from $currentlyVisible -> $index") return animateToolbarsVisibility(currentlyVisible, index).also { currentlyVisible = index } } @@ -61,14 +62,15 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr val targetFromAlpha = 0f val targetToAlpha = 1f val targetDuration = + // Since this view starts with the lowest toolbar index, if (from < to) { + logD("Moving higher, use an entrance animation") context.getInteger(R.integer.anim_fade_enter_duration).toLong() } else { + logD("Moving lower, use an exit animation") context.getInteger(R.integer.anim_fade_exit_duration).toLong() } - logD(targetDuration) - val fromView = getChildAt(from) as Toolbar val toView = getChildAt(to) as Toolbar @@ -80,15 +82,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (!isLaidOut) { // Not laid out, just change it immediately while are not shown to the user. // This is an initialization, so we return false despite changing. + logD("Not laid out, immediately updating visibility") setToolbarsAlpha(fromView, toView, targetFromAlpha) return false } - if (fadeThroughAnimator != null) { - fadeThroughAnimator?.cancel() - fadeThroughAnimator = null - } - + logD("Changing toolbar visibility $from -> 0f, $to -> 1f") + fadeThroughAnimator?.cancel() fadeThroughAnimator = ValueAnimator.ofFloat(fromView.alpha, targetFromAlpha).apply { duration = targetDuration @@ -100,7 +100,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } private fun setToolbarsAlpha(from: Toolbar, to: Toolbar, innerAlpha: Float) { - logD("${to.id == R.id.detail_edit_toolbar} ${1 - innerAlpha}") from.apply { alpha = innerAlpha isInvisible = innerAlpha == 0f 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 13bcbcbf9..dc699498a 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt @@ -96,6 +96,7 @@ class UISettingsImpl @Inject constructor(@ApplicationContext context: Context) : override fun onSettingChanged(key: String, listener: UISettings.Listener) { if (key == getString(R.string.set_key_round_mode)) { + logD("Dispatching round mode setting change") listener.onRoundModeChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 55939af17..eb24d8093 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -36,7 +36,6 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import java.lang.IllegalArgumentException -import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song @@ -289,6 +288,7 @@ fun Context.share(parent: MusicParent) = share(parent.songs) */ fun Context.share(songs: List) { if (songs.isEmpty()) return + logD("Showing sharesheet for ${songs.size} songs") val builder = ShareCompat.IntentBuilder(this) val mimeTypes = mutableSetOf() for (song in songs) { diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index 6c1e9e91b..8aba7f42c 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -76,6 +76,7 @@ constructor( val repeatMode = playbackManager.repeatMode val isShuffled = playbackManager.queue.isShuffled + logD("Updating widget with new playback state") bitmapProvider.load( song, object : BitmapProvider.Target { @@ -83,12 +84,15 @@ constructor( val cornerRadius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12, always round the cover with the widget's inner radius + logD("Using android 12 corner radius") context.getDimenPixels(android.R.dimen.system_app_widget_inner_radius) } else if (uiSettings.roundMode) { // < Android 12, but the user still enabled round mode. + logD("Using default corner radius") context.getDimenPixels(R.dimen.size_corners_medium) } else { // User did not enable round mode. + logD("Using no corner radius") 0 } @@ -107,6 +111,7 @@ constructor( override fun onCompleted(bitmap: Bitmap?) { val state = PlaybackState(song, bitmap, isPlaying, repeatMode, isShuffled) + logD("Bitmap loaded, uploading state $state") widgetProvider.update(context, uiSettings, state) } }) diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index ececdf6e2..153b7bccf 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -81,6 +81,7 @@ class WidgetProvider : AppWidgetProvider() { fun update(context: Context, uiSettings: UISettings, state: WidgetComponent.PlaybackState?) { if (state == null) { // No state, use the default widget. + logD("No state provided, returning to default") reset(context) return } @@ -101,6 +102,7 @@ class WidgetProvider : AppWidgetProvider() { val component = ComponentName(context, this::class.java) try { awm.updateAppWidgetCompat(context, component, views) + logD("Successfully updated RemoteViews layout") } catch (e: Exception) { // Layout update failed, gracefully degrade to the default widget. logW("Unable to update widget: $e") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 139cc91c9..676133e13 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -338,7 +338,6 @@ No track No songs No music playing - There\'s nothing here yet diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt index 57aae6970..1c322e36a 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt @@ -100,8 +100,6 @@ open class FakeArtist : Artist { open class FakeGenre : Genre { override val name: Name get() = throw NotImplementedError() - override val albums: List - get() = throw NotImplementedError() override val artists: List get() = throw NotImplementedError() override val durationMs: Long From 6fc743990d362c86a00b50241aa9ec808d50edf9 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 26 May 2023 16:39:24 -0600 Subject: [PATCH 14/43] actions: disable tests for now I need to do them right before enabling this. --- .github/workflows/android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f106a3aac..f0aff366e 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -23,8 +23,8 @@ jobs: cache: gradle - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Test app with Gradle - run: ./gradlew app:testDebug + # - name: Test app with Gradle + # run: ./gradlew app:testDebug - name: Build debug APK with Gradle run: ./gradlew app:packageDebug - name: Upload debug APK artifact From d539c3551882be5be1292a7ec0b833ca1a61543d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 28 May 2023 09:06:41 -0600 Subject: [PATCH 15/43] ui: fix button log spam Fix log spam about unresolved attrs coming from button apparently forgetting where it's default icon tint is. --- app/src/main/java/org/oxycblt/auxio/MainActivity.kt | 1 - app/src/main/res/values/styles_ui.xml | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 62e6a7c23..a5df4f5b4 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -51,7 +51,6 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * TODO: Unit testing * TODO: Fix UID naming * TODO: Leverage FlexibleListAdapter more in dialogs (Disable item anims) - * TODO: Add more logging * TODO: Try to move on from synchronized and volatile in shared objs */ @AndroidEntryPoint diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 6e0ee0001..acc55daa7 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -220,6 +220,8 @@ @dimen/spacing_small @dimen/spacing_small @dimen/spacing_small + + @color/m3_text_button_foreground_color_selector - \ No newline at end of file diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml index bbe18c90c..446c3064f 100644 --- a/app/src/main/res/values/styles_core.xml +++ b/app/src/main/res/values/styles_core.xml @@ -50,9 +50,15 @@ false true + + @color/sel_compat_ripple ?attr/colorOnSurfaceVariant ?attr/colorPrimary + + @color/overlay_text_highlight + @color/overlay_text_highlight_inverse + @style/PreferenceTheme.Auxio @style/Preference.Auxio @style/Preference.Auxio.PreferenceCategory From 46fb33de5985cadb4255a4b323d760bf8bc2cba1 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 1 Jun 2023 12:37:47 -0600 Subject: [PATCH 30/43] image: optimize grouping routine Turns out the image grouping routine runs on the main thread and is slow enough to cause massive freezing. Attempt to resolve this. --- .../java/org/oxycblt/auxio/image/CoverView.kt | 6 +++++- .../auxio/image/extractor/CoverExtractor.kt | 20 +++++++++++-------- .../org/oxycblt/auxio/util/ContextUtil.kt | 1 - 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index 63f773cf9..a4d0e6917 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -322,7 +322,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * * @param song The [Song] to bind to the view. */ - fun bind(song: Song) = bind(song.album) + fun bind(song: Song) = + bind( + listOf(song), + context.getString(R.string.desc_album_cover, song.album.name), + R.drawable.ic_album_24) /** * Bind an [Album]'s image to this view. diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index ef9af4c3f..0f0b3fbb7 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -117,13 +117,17 @@ constructor( * by their names. "Representation" is defined by how many [Song]s were found to be linked to * the given [Album] in the given [Song] list. */ - fun computeCoverOrdering(songs: List) = - Sort(Sort.Mode.ByAlbum, Sort.Direction.ASCENDING) - .songs(songs) - .groupBy { it.album } - .entries - .sortedByDescending { it.value.size } - .map { it.key } + fun computeCoverOrdering(songs: List): List { + if (songs.isEmpty()) return listOf() + if (songs.size == 1) return listOf(songs.first().album) + + val sortedMap = + sortedMapOf(Sort.Mode.ByName.getAlbumComparator(Sort.Direction.ASCENDING)) + for (song in songs) { + sortedMap[song.album] = (sortedMap[song.album] ?: 0) + 1 + } + return sortedMap.keys.sortedByDescending { sortedMap[it] } + } private suspend fun openCoverInputStream(album: Album) = try { @@ -205,7 +209,7 @@ constructor( withContext(Dispatchers.IO) { context.contentResolver.openInputStream(album.coverUri) } /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ - private suspend fun createMosaic(streams: List, size: Size): FetchResult { + private fun createMosaic(streams: List, size: Size): FetchResult { // Use whatever size coil gives us to create the mosaic. val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize()) val mosaicFrameSize = diff --git a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt index c8bcd6a9c..bf6d9313b 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -23,7 +23,6 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration -import android.os.Build import android.util.TypedValue import android.view.LayoutInflater import android.widget.Toast From f9ccb831d882f4d5086ee2b1b7e37c4098132c77 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 1 Jun 2023 15:05:24 -0600 Subject: [PATCH 31/43] music: fix comparison issues Fix a few problems with the current comparison algorithm: 1. It wasn't actually comparing the raw music information, only the UIDs, which was redundant. 2. The comparison in the main music loading process occurred on the main thread, which causes massive freeze-up issues. Resolves #457. --- CHANGELOG.md | 2 + .../auxio/image/extractor/CoverExtractor.kt | 1 + .../oxycblt/auxio/music/MusicRepository.kt | 50 ++++--- .../auxio/music/device/DeviceLibrary.kt | 18 +-- .../auxio/music/device/DeviceMusicImpl.kt | 6 +- .../oxycblt/auxio/music/device/RawMusic.kt | 130 ++++++++++-------- .../oxycblt/auxio/music/user/UserLibrary.kt | 2 + .../auxio/playback/system/PlaybackService.kt | 4 +- 8 files changed, 118 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06e0002f3..adec5f29e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ within it - Fixed blurry playing indicator in album/artist/genre/playlist items - Fixed incorrect songs being displayed when adding albums to the end of the queue +- Fixed freezing occuring when scrolling through large music libraries +- Fixed app not responding once music loading completes for large libraries #### What's Changed - Android Lollipop and Marshmallow support have been dropped diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index 0f0b3fbb7..067b8361c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -118,6 +118,7 @@ constructor( * the given [Album] in the given [Song] list. */ fun computeCoverOrdering(songs: List): List { + // TODO: Start short-circuiting in more places if (songs.isEmpty()) return listOf() if (songs.size == 1) return listOf(songs.first().album) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index c88eaaf14..8d890b218 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import org.oxycblt.auxio.music.cache.CacheRepository import org.oxycblt.auxio.music.device.DeviceLibrary @@ -301,44 +300,35 @@ constructor( val userLibrary = synchronized(this) { userLibrary ?: return } logD("Creating playlist $name with ${songs.size} songs") userLibrary.createPlaylist(name, songs) - notifyUserLibraryChange() + emitLibraryChange(device = false, user = true) } override suspend fun renamePlaylist(playlist: Playlist, name: String) { val userLibrary = synchronized(this) { userLibrary ?: return } logD("Renaming $playlist to $name") userLibrary.renamePlaylist(playlist, name) - notifyUserLibraryChange() + emitLibraryChange(device = false, user = true) } override suspend fun deletePlaylist(playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } logD("Deleting $playlist") userLibrary.deletePlaylist(playlist) - notifyUserLibraryChange() + emitLibraryChange(device = false, user = true) } override suspend fun addToPlaylist(songs: List, playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } logD("Adding ${songs.size} songs to $playlist") userLibrary.addToPlaylist(playlist, songs) - notifyUserLibraryChange() + emitLibraryChange(device = false, user = true) } override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { val userLibrary = synchronized(this) { userLibrary ?: return } logD("Rewriting $playlist with ${songs.size} songs") userLibrary.rewritePlaylist(playlist, songs) - notifyUserLibraryChange() - } - - @Synchronized - private fun notifyUserLibraryChange() { - logD("Dispatching user library change") - for (listener in updateListeners) { - listener.onMusicChanges( - MusicRepository.Changes(deviceLibrary = false, userLibrary = true)) - } + emitLibraryChange(device = false, user = true) } @Synchronized @@ -435,7 +425,7 @@ constructor( val deviceLibraryChannel = Channel() logD("Starting DeviceLibrary creation") val deviceLibraryJob = - worker.scope.tryAsync(Dispatchers.Main) { + worker.scope.tryAsync(Dispatchers.Default) { deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) } } logD("Starting UserLibrary creation") @@ -452,10 +442,22 @@ constructor( val userLibrary = userLibraryJob.await().getOrThrow() logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]") - withContext(Dispatchers.Main) { - emitComplete(null) - emitData(deviceLibrary, userLibrary) + emitComplete(null) + + // Comparing the library instances is obscenely expensive, do it within the library + val deviceLibraryChanged = this.deviceLibrary != deviceLibrary + val userLibraryChanged = this.userLibrary != userLibrary + if (!deviceLibraryChanged && !userLibraryChanged) { + logD("Library has not changed, skipping update") + return } + + synchronized(this) { + this.deviceLibrary = deviceLibrary + this.userLibrary = userLibrary + } + + emitLibraryChange(deviceLibraryChanged, userLibraryChanged) } /** @@ -497,14 +499,8 @@ constructor( } @Synchronized - private fun emitData(deviceLibrary: DeviceLibrary, userLibrary: MutableUserLibrary) { - val deviceLibraryChanged = this.deviceLibrary != deviceLibrary - val userLibraryChanged = this.userLibrary != userLibrary - if (!deviceLibraryChanged && !userLibraryChanged) return - - this.deviceLibrary = deviceLibrary - this.userLibrary = userLibrary - val changes = MusicRepository.Changes(deviceLibraryChanged, userLibraryChanged) + private fun emitLibraryChange(device: Boolean, user: Boolean) { + val changes = MusicRepository.Changes(device, user) logD("Dispatching library change [changes=$changes]") for (listener in updateListeners) { listener.onMusicChanges(changes) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 66d1c5d77..9add74b28 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -193,8 +193,8 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings private fun buildAlbums(songs: List, settings: MusicSettings): List { // Group songs by their singular raw album, then map the raw instances and their // grouped songs to Album values. Album.Raw will handle the actual grouping rules. - val songsByAlbum = songs.groupBy { it.rawAlbum } - val albums = songsByAlbum.map { AlbumImpl(it.key, settings, it.value) } + val songsByAlbum = songs.groupBy { it.rawAlbum.key } + val albums = songsByAlbum.map { AlbumImpl(it.key.value, settings, it.value) } logD("Successfully built ${albums.size} albums") return albums } @@ -221,22 +221,22 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings ): List { // Add every raw artist credited to each Song/Album to the grouping. This way, // different multi-artist combinations are not treated as different artists. - val musicByArtist = mutableMapOf>() + val musicByArtist = mutableMapOf>() for (song in songs) { for (rawArtist in song.rawArtists) { - musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(song) + musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(song) } } for (album in albums) { for (rawArtist in album.rawArtists) { - musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(album) + musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(album) } } // Convert the combined mapping into artist instances. - val artists = musicByArtist.map { ArtistImpl(it.key, settings, it.value) } + val artists = musicByArtist.map { ArtistImpl(it.key.value, settings, it.value) } logD("Successfully built ${artists.size} artists") return artists } @@ -253,15 +253,15 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings private fun buildGenres(songs: List, settings: MusicSettings): List { // Add every raw genre credited to each Song to the grouping. This way, // different multi-genre combinations are not treated as different genres. - val songsByGenre = mutableMapOf>() + val songsByGenre = mutableMapOf>() for (song in songs) { for (rawGenre in song.rawGenres) { - songsByGenre.getOrPut(rawGenre) { mutableListOf() }.add(song) + songsByGenre.getOrPut(rawGenre.key) { mutableListOf() }.add(song) } } // Convert the mapping into genre instances. - val genres = songsByGenre.map { GenreImpl(it.key, settings, it.value) } + val genres = songsByGenre.map { GenreImpl(it.key.value, settings, it.value) } logD("Successfully built ${genres.size} genres") return genres } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index c07c5a65f..ed73bf885 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -407,7 +407,8 @@ class ArtistImpl( * [RawArtist] will be within the list. * @return The index of the [Artist]'s [RawArtist] within the list. */ - fun getOriginalPositionIn(rawArtists: List) = rawArtists.indexOf(rawArtist) + fun getOriginalPositionIn(rawArtists: List) = + rawArtists.indexOfFirst { it.key == rawArtist.key } /** * Perform final validation and organization on this instance. @@ -481,7 +482,8 @@ class GenreImpl( * [RawGenre] will be within the list. * @return The index of the [Genre]'s [RawGenre] within the list. */ - fun getOriginalPositionIn(rawGenres: List) = rawGenres.indexOf(rawGenre) + fun getOriginalPositionIn(rawGenres: List) = + rawGenres.indexOfFirst { it.key == rawGenre.key } /** * Perform final validation and organization on this instance. diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 10d248991..2a198c687 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -113,28 +113,35 @@ data class RawAlbum( /** @see RawArtist.name */ val rawArtists: List ) { - // Albums are grouped as follows: - // - If we have a MusicBrainz ID, only group by it. This allows different Albums with the - // same name to be differentiated, which is common in large libraries. - // - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase - // artist name. This allows for case-insensitive artist/album grouping, which can be common - // for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein"). + val key = Key(this) - // Cache the hash-code for HashMap efficiency. - private val hashCode = - musicBrainzId?.hashCode() ?: (31 * name.lowercase().hashCode() + rawArtists.hashCode()) + /** Exposed information that denotes [RawAlbum] uniqueness. */ + data class Key(val value: RawAlbum) { + // Albums are grouped as follows: + // - If we have a MusicBrainz ID, only group by it. This allows different Albums with the + // same name to be differentiated, which is common in large libraries. + // - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase + // artist name. This allows for case-insensitive artist/album grouping, which can be common + // for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein"). - override fun hashCode() = hashCode + // Cache the hash-code for HashMap efficiency. + private val hashCode = + value.musicBrainzId?.hashCode() + ?: (31 * value.name.lowercase().hashCode() + value.rawArtists.hashCode()) - override fun equals(other: Any?) = - other is RawAlbum && - when { - musicBrainzId != null && other.musicBrainzId != null -> - musicBrainzId == other.musicBrainzId - musicBrainzId == null && other.musicBrainzId == null -> - name.equals(other.name, true) && rawArtists == other.rawArtists - else -> false - } + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is Key && + when { + value.musicBrainzId != null && other.value.musicBrainzId != null -> + value.musicBrainzId == other.value.musicBrainzId + value.musicBrainzId == null && other.value.musicBrainzId == null -> + other.value.name.equals(other.value.name, true) && + other.value.rawArtists == other.value.rawArtists + else -> false + } + } } /** @@ -151,33 +158,42 @@ data class RawArtist( /** @see Music.name */ val sortName: String? = null ) { - // Artists are grouped as follows: - // - If we have a MusicBrainz ID, only group by it. This allows different Artists with the - // same name to be differentiated, which is common in large libraries. - // - If we do not have a MusicBrainz ID, compare by the lowercase name. This allows artist - // grouping to be case-insensitive. + val key = Key(this) - // Cache the hashCode for HashMap efficiency. - private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode() + /** + * Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on + * an item-by-item + */ + data class Key(val value: RawArtist) { + // Artists are grouped as follows: + // - If we have a MusicBrainz ID, only group by it. This allows different Artists with the + // same name to be differentiated, which is common in large libraries. + // - If we do not have a MusicBrainz ID, compare by the lowercase name. This allows artist + // grouping to be case-insensitive. - // Compare names and MusicBrainz IDs in order to differentiate artists with the - // same name in large libraries. + // Cache the hashCode for HashMap efficiency. + private val hashCode = value.musicBrainzId?.hashCode() ?: value.name?.lowercase().hashCode() - override fun hashCode() = hashCode + // Compare names and MusicBrainz IDs in order to differentiate artists with the + // same name in large libraries. - override fun equals(other: Any?) = - other is RawArtist && - when { - musicBrainzId != null && other.musicBrainzId != null -> - musicBrainzId == other.musicBrainzId - musicBrainzId == null && other.musicBrainzId == null -> - when { - name != null && other.name != null -> name.equals(other.name, true) - name == null && other.name == null -> true - else -> false - } - else -> false - } + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is Key && + when { + value.musicBrainzId != null && other.value.musicBrainzId != null -> + value.musicBrainzId == other.value.musicBrainzId + value.musicBrainzId == null && other.value.musicBrainzId == null -> + when { + value.name != null && other.value.name != null -> + value.name.equals(other.value.name, true) + value.name == null && other.value.name == null -> true + else -> false + } + else -> false + } + } } /** @@ -189,20 +205,24 @@ data class RawGenre( /** @see Music.name */ val name: String? = null ) { + val key = Key(this) - // Cache the hashCode for HashMap efficiency. - private val hashCode = name?.lowercase().hashCode() + data class Key(val value: RawGenre) { + // Cache the hashCode for HashMap efficiency. + private val hashCode = value.name?.lowercase().hashCode() - // Only group by the lowercase genre name. This allows Genre grouping to be - // case-insensitive, which may be helpful in some libraries with different ways of - // formatting genres. - override fun hashCode() = hashCode + // Only group by the lowercase genre name. This allows Genre grouping to be + // case-insensitive, which may be helpful in some libraries with different ways of + // formatting genres. + override fun hashCode() = hashCode - override fun equals(other: Any?) = - other is RawGenre && - when { - name != null && other.name != null -> name.equals(other.name, true) - name == null && other.name == null -> true - else -> false - } + override fun equals(other: Any?) = + other is Key && + when { + value.name != null && other.value.name != null -> + value.name.equals(other.value.name, true) + value.name == null && other.value.name == null -> true + else -> false + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index f4caab962..f6a888eb7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -149,6 +149,8 @@ private class UserLibraryImpl( private val playlistMap: MutableMap, private val musicSettings: MusicSettings ) : MutableUserLibrary { + override fun hashCode() = playlistMap.hashCode() + override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap override fun toString() = "UserLibrary(playlists=${playlists.size})" override val playlists: List diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index d4ceaa88f..c1dbbe7c2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -107,8 +107,8 @@ class PlaybackService : // Coroutines private val serviceJob = Job() - private val restoreScope = CoroutineScope(serviceJob + Dispatchers.Main) - private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main) + private val restoreScope = CoroutineScope(serviceJob + Dispatchers.IO) + private val saveScope = CoroutineScope(serviceJob + Dispatchers.IO) // --- SERVICE OVERRIDES --- From a37df594e74c4caabf1b8e0488c6e3c1b8bc31ce Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 1 Jun 2023 20:13:52 -0600 Subject: [PATCH 32/43] music: fix song build bottleneck Fix redundant separator parsing obliterating loading performance. If there are no separators configured, the parsing function would not short-circuit and instead do a useless O(n) iteraton (including escaping!), massively reducing performance. Song build performance still isn't the best (blame intelligent sorting), but it's definitely better now. --- CHANGELOG.md | 1 + .../main/java/org/oxycblt/auxio/music/MusicRepository.kt | 4 ++++ .../java/org/oxycblt/auxio/music/device/DeviceLibrary.kt | 7 +++++-- app/src/main/java/org/oxycblt/auxio/music/info/Name.kt | 2 ++ .../main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt | 4 +++- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adec5f29e..8239410b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Albums implicitly linked only via "artist" tags are now placed in a special "appears on" section in the artist view - Album covers that are not 1:1 aspect ratio are no longer cropped +- Optimized library creation phase of the music loading process #### What's Fixed - Prevented options such as "Add to queue" from being selected on empty artists and playlists diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 8d890b218..36133c94c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -365,6 +365,10 @@ constructor( throw NoAudioPermissionException() } + // TODO: Parallelize this even more aggressively. I can hypothetically connect all + // finalization steps (library, cache, playlists) into a single pipeline would need + // to change how I indicate progress however + // Start initializing the extractors. Use an indeterminate state, as there is no ETA on // how long a media database query will take. emitLoading(IndexingProgress.Indeterminate) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 9add74b28..ef846c18d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -177,9 +177,12 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings * @return A sorted list of [SongImpl]s derived from the [RawSong] that should be suitable for * grouping. */ - private fun buildSongs(rawSongs: List, settings: MusicSettings) = - Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) + private fun buildSongs(rawSongs: List, settings: MusicSettings): List { + val songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) .songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid }) + logD("Successfully built ${songs.size} songs") + return songs + } /** * Build a list of [Album]s from the given [Song]s. diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt index 6b508f56a..838f8f5d5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -174,6 +174,8 @@ private data class IntelligentKnownName(override val raw: String, override val s override val sortTokens = parseTokens(sort ?: raw) private fun parseTokens(name: String): List { + // TODO: This routine is consuming much of the song building runtime, find a way to + // optimize it val stripped = name // Remove excess punctuation from the string, as those u diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt index 62f19c61b..c4f20df4a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt @@ -39,6 +39,8 @@ fun List.parseMultiValue(settings: MusicSettings) = this } +// TODO: Remove the escaping checks, it's too expensive to do this for every single tag. + /** * Split a [String] by the given selector, automatically handling escaped characters that satisfy * the selector. @@ -106,7 +108,7 @@ fun List.correctWhitespace() = mapNotNull { it.correctWhitespace() } * @return A list of one or more [String]s that were split up by the user-defined separators. */ private fun String.maybeParseBySeparators(settings: MusicSettings): List { - // Get the separators the user desires. If null, there's nothing to do. + if (settings.multiValueSeparators.isEmpty()) return listOf(this) return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace() } From 0d28bdf99e37672d80f42a406343e4624c18cbcb Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 1 Jun 2023 20:18:02 -0600 Subject: [PATCH 33/43] music: remove unnecessary documentation Remove unnecessary function documentation for private methods in DeviceLibrary. --- .../auxio/music/device/DeviceLibrary.kt | 49 ++----------------- 1 file changed, 5 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index ef846c18d..b53d0a00b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -169,30 +169,14 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings songs.find { it.path.name == displayName && it.size == size } } - /** - * Build a list [SongImpl]s from the given [RawSong]. - * - * @param rawSongs The [RawSong]s to build the [SongImpl]s from. - * @param settings [MusicSettings] to obtain user parsing configuration. - * @return A sorted list of [SongImpl]s derived from the [RawSong] that should be suitable for - * grouping. - */ - private fun buildSongs(rawSongs: List, settings: MusicSettings): List { - val songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) - .songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid }) + private fun buildSongs(rawSongs: List, settings: MusicSettings): List { + val songs = + Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) + .songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid }) logD("Successfully built ${songs.size} songs") return songs } - /** - * Build a list of [Album]s from the given [Song]s. - * - * @param songs The [Song]s to build [Album]s from. These will be linked with their respective - * [Album]s when created. - * @param settings [MusicSettings] to obtain user parsing configuration. - * @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked - * with parent [Artist] instances in order to be usable. - */ private fun buildAlbums(songs: List, settings: MusicSettings): List { // Group songs by their singular raw album, then map the raw instances and their // grouped songs to Album values. Album.Raw will handle the actual grouping rules. @@ -202,21 +186,6 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings return albums } - /** - * Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as - * they group into [Artist] instances much differently, with [Song]s being grouped primarily by - * artist names, and [Album]s being grouped primarily by album artist names. - * - * @param songs The [Song]s to build [Artist]s from. One [Song] can result in the creation of - * one or more [Artist] instances. These will be linked with their respective [Artist]s when - * created. - * @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of - * one or more [Artist] instances. These will be linked with their respective [Artist]s when - * created. - * @param settings [MusicSettings] to obtain user parsing configuration. - * @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings - * of [Song]s and [Album]s. - */ private fun buildArtists( songs: List, albums: List, @@ -224,6 +193,7 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings ): List { // Add every raw artist credited to each Song/Album to the grouping. This way, // different multi-artist combinations are not treated as different artists. + // Songs and albums are grouped by artist and album artist respectively. val musicByArtist = mutableMapOf>() for (song in songs) { @@ -244,15 +214,6 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings return artists } - /** - * Group up [Song]s into [Genre] instances. - * - * @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of - * one or more [Genre] instances. These will be linked with their respective [Genre]s when - * created. - * @param settings [MusicSettings] to obtain user parsing configuration. - * @return A non-empty list of [Genre]s. - */ private fun buildGenres(songs: List, settings: MusicSettings): List { // Add every raw genre credited to each Song to the grouping. This way, // different multi-genre combinations are not treated as different genres. From df174e22f67847d77d3e716c43c1a9982f5e3a7a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 1 Jun 2023 20:34:21 -0600 Subject: [PATCH 34/43] music: cache hashcode in data Cache the hashcode of song/album/artist/genre information so that it can be calculated easily later. --- .../auxio/music/device/DeviceLibrary.kt | 13 ++- .../auxio/music/device/DeviceMusicImpl.kt | 81 +++++++++---------- .../auxio/music/fs/MediaStoreExtractor.kt | 2 +- .../java/org/oxycblt/auxio/music/info/Date.kt | 7 +- .../oxycblt/auxio/music/user/PlaylistImpl.kt | 23 +++--- 5 files changed, 63 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index b53d0a00b..96f4915b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -170,19 +170,21 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings } private fun buildSongs(rawSongs: List, settings: MusicSettings): List { + val start = System.currentTimeMillis() val songs = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) .songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid }) - logD("Successfully built ${songs.size} songs") + logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms") return songs } private fun buildAlbums(songs: List, settings: MusicSettings): List { + val start = System.currentTimeMillis() // Group songs by their singular raw album, then map the raw instances and their // grouped songs to Album values. Album.Raw will handle the actual grouping rules. val songsByAlbum = songs.groupBy { it.rawAlbum.key } val albums = songsByAlbum.map { AlbumImpl(it.key.value, settings, it.value) } - logD("Successfully built ${albums.size} albums") + logD("Successfully built ${albums.size} albums in ${System.currentTimeMillis() - start}ms") return albums } @@ -191,6 +193,7 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings albums: List, settings: MusicSettings ): List { + val start = System.currentTimeMillis() // Add every raw artist credited to each Song/Album to the grouping. This way, // different multi-artist combinations are not treated as different artists. // Songs and albums are grouped by artist and album artist respectively. @@ -210,11 +213,13 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings // Convert the combined mapping into artist instances. val artists = musicByArtist.map { ArtistImpl(it.key.value, settings, it.value) } - logD("Successfully built ${artists.size} artists") + logD( + "Successfully built ${artists.size} artists in ${System.currentTimeMillis() - start}ms") return artists } private fun buildGenres(songs: List, settings: MusicSettings): List { + val start = System.currentTimeMillis() // Add every raw genre credited to each Song to the grouping. This way, // different multi-genre combinations are not treated as different genres. val songsByGenre = mutableMapOf>() @@ -226,7 +231,7 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings // Convert the mapping into genre instances. val genres = songsByGenre.map { GenreImpl(it.key.value, settings, it.value) } - logD("Successfully built ${genres.size} genres") + logD("Successfully built ${genres.size} genres in ${System.currentTimeMillis() - start}ms") return genres } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index ed73bf885..530f35530 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -93,7 +93,9 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son override val album: Album get() = unlikelyToBeNull(_album) - override fun hashCode() = 31 * uid.hashCode() + rawSong.hashCode() + private val hashCode = 31 * uid.hashCode() + rawSong.hashCode() + + override fun hashCode() = hashCode override fun equals(other: Any?) = other is SongImpl && uid == other.uid && rawSong == other.rawSong override fun toString() = "Song(uid=$uid, name=$name)" @@ -253,22 +255,12 @@ class AlbumImpl( override val durationMs: Long override val dateAdded: Long - override fun hashCode(): Int { - var hashCode = uid.hashCode() - hashCode = 31 * hashCode + rawAlbum.hashCode() - hashCode = 31 * hashCode + songs.hashCode() - return hashCode - } - - override fun equals(other: Any?) = - other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs - - override fun toString() = "Album(uid=$uid, name=$name)" - private val _artists = mutableListOf() override val artists: List get() = _artists + private var hashCode = uid.hashCode() + init { var totalDuration: Long = 0 var earliestDateAdded: Long = Long.MAX_VALUE @@ -284,8 +276,16 @@ class AlbumImpl( durationMs = totalDuration dateAdded = earliestDateAdded + + hashCode = 31 * hashCode + rawAlbum.hashCode() + hashCode = 31 * hashCode + songs.hashCode() } + override fun hashCode() = hashCode + override fun equals(other: Any?) = + other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs + override fun toString() = "Album(uid=$uid, name=$name)" + /** * The [RawArtist] instances collated by the [Album]. The album artists of the song take * priority, followed by the artists. If there are no artists, this field will be a single @@ -351,25 +351,10 @@ class ArtistImpl( override val implicitAlbums: List override val durationMs: Long? - // Note: Append song contents to MusicParent equality so that artists with - // the same UID but different songs are not equal. - override fun hashCode(): Int { - var hashCode = uid.hashCode() - hashCode = 31 * hashCode + rawArtist.hashCode() - hashCode = 31 * hashCode + songs.hashCode() - return hashCode - } - - override fun equals(other: Any?) = - other is ArtistImpl && - uid == other.uid && - rawArtist == other.rawArtist && - songs == other.songs - - override fun toString() = "Artist(uid=$uid, name=$name)" - override lateinit var genres: List + private var hashCode = uid.hashCode() + init { val distinctSongs = mutableSetOf() val albumMap = mutableMapOf() @@ -396,8 +381,23 @@ class ArtistImpl( explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) } implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) } durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull() + + hashCode = 31 * hashCode + rawArtist.hashCode() + hashCode = 31 * hashCode + songs.hashCode() } + // Note: Append song contents to MusicParent equality so that artists with + // the same UID but different songs are not equal. + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is ArtistImpl && + uid == other.uid && + rawArtist == other.rawArtist && + songs == other.songs + + override fun toString() = "Artist(uid=$uid, name=$name)" + /** * Returns the original position of this [Artist]'s [RawArtist] within the given [RawArtist] * list. This can be used to create a consistent ordering within child [Artist] lists based on @@ -445,17 +445,7 @@ class GenreImpl( override val artists: List override val durationMs: Long - override fun hashCode(): Int { - var hashCode = uid.hashCode() - hashCode = 31 * hashCode + rawGenre.hashCode() - hashCode = 31 * hashCode + songs.hashCode() - return hashCode - } - - override fun equals(other: Any?) = - other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs - - override fun toString() = "Genre(uid=$uid, name=$name)" + private var hashCode = uid.hashCode() init { val distinctAlbums = mutableSetOf() @@ -471,8 +461,17 @@ class GenreImpl( artists = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).artists(distinctArtists) durationMs = totalDuration + hashCode = 31 * hashCode + rawGenre.hashCode() + hashCode = 31 * hashCode + songs.hashCode() } + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs + + override fun toString() = "Genre(uid=$uid, name=$name)" + /** * Returns the original position of this [Genre]'s [RawGenre] within the given [RawGenre] list. * This can be used to create a consistent ordering within child [Genre] lists based on the diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index 4e8faae19..42e29eed2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -186,8 +186,8 @@ private abstract class BaseMediaStoreExtractor( } } } - logD("Read ${genreNamesMap.size} genres from MediaStore") + logD("Read ${genreNamesMap.values.distinct().size} genres from MediaStore") logD("Finished initialization in ${System.currentTimeMillis() - start}ms") return wrapQuery(cursor, genreNamesMap) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt index 1d717ad43..b47f580db 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt @@ -73,10 +73,9 @@ class Date private constructor(private val tokens: List) : Comparable return format.format(date) } - override fun hashCode() = tokens.hashCode() - override fun equals(other: Any?) = other is Date && compareTo(other) == 0 - + override fun hashCode() = tokens.hashCode() + override fun toString() = StringBuilder().appendDate().toString() override fun compareTo(other: Date): Int { for (i in 0 until max(tokens.size, other.tokens.size)) { val ai = tokens.getOrNull(i) @@ -97,8 +96,6 @@ class Date private constructor(private val tokens: List) : Comparable return 0 } - override fun toString() = StringBuilder().appendDate().toString() - private fun StringBuilder.appendDate(): StringBuilder { // Construct an ISO-8601 date, dropping precision that doesn't exist. append(year.toStringFixed(4)) diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index 5f7036c5f..9ad14f411 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -33,6 +33,17 @@ private constructor( override val songs: List ) : Playlist { override val durationMs = songs.sumOf { it.durationMs } + private var hashCode = uid.hashCode() + + init { + hashCode = 31 * hashCode + name.hashCode() + hashCode = 31 * hashCode + songs.hashCode() + } + + override fun equals(other: Any?) = + other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs + override fun hashCode() = hashCode + override fun toString() = "Playlist(uid=$uid, name=$name)" /** * Clone the data in this instance to a new [PlaylistImpl] with the given [name]. @@ -57,18 +68,6 @@ private constructor( */ inline fun edit(edits: MutableList.() -> Unit) = edit(songs.toMutableList().apply(edits)) - override fun equals(other: Any?) = - other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs - - override fun hashCode(): Int { - var hashCode = uid.hashCode() - hashCode = 31 * hashCode + name.hashCode() - hashCode = 31 * hashCode + songs.hashCode() - return hashCode - } - - override fun toString() = "Playlist(uid=$uid, name=$name)" - companion object { /** * Create a new instance with a novel UID. From 4581532928fa038c3ad63f477a2b64e1a1576942 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 2 Jun 2023 13:34:00 -0600 Subject: [PATCH 35/43] queue: shift back if removing playing end Shift the queue index backwards if the current song at the end of a queue is removed. Without this, the index becomes OOB and makes the queue be interpreted as entirely empty when it actually isn't, which is compounded by a remove-1 update intruction leading to a RecyclerView inconsistency crash. --- .../main/java/org/oxycblt/auxio/playback/queue/Queue.kt | 9 +++++++-- .../oxycblt/auxio/playback/state/PlaybackStateManager.kt | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt index 9d9352ef4..c5ebf9904 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt @@ -107,7 +107,7 @@ interface Queue { } } -class EditableQueue : Queue { +class MutableQueue : Queue { @Volatile private var heap = mutableListOf() @Volatile private var orderedMapping = mutableListOf() @Volatile private var shuffledMapping = mutableListOf() @@ -302,6 +302,7 @@ class EditableQueue : Queue { * @return A [Queue.Change] instance that reflects the changes made. */ fun remove(at: Int): Queue.Change { + val lastIndex = orderedMapping.lastIndex if (shuffledMapping.isNotEmpty()) { // Remove the specified index in the shuffled mapping and the analogous song in the // ordered mapping. @@ -321,12 +322,16 @@ class EditableQueue : Queue { // We just removed the currently playing song. index == at -> { logD("Removed current song") + if (lastIndex == index) { + logD("Current song at end of queue, shift back") + --index + } Queue.Change.Type.SONG } // Index was ahead of removed song, shift back to preserve consistency. index > at -> { logD("Removed before current song, shift back") - index -= 1 + --index Queue.Change.Type.INDEX } // Nothing to do diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 4f2dbb5c0..388087653 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -22,7 +22,7 @@ import javax.inject.Inject import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.playback.queue.EditableQueue +import org.oxycblt.auxio.playback.queue.MutableQueue import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -305,7 +305,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Volatile private var pendingAction: InternalPlayer.Action? = null @Volatile private var isInitialized = false - override val queue = EditableQueue() + override val queue = MutableQueue() @Volatile override var parent: MusicParent? = null private set From e39d4d879c97e870451949b61146b75198fa6532 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 2 Jun 2023 14:25:56 -0600 Subject: [PATCH 36/43] ui: fix stuck sheet when playback ends Fix an issue where the playback sheet will suddenly become "stuck" when playback ends, preventing back navigation or playing new tracks. This is, once again, caused by bottom sheet insanity. It does not expose the target state of a sheet at all when it's settling, making Auxio believe that it has to repeatedly "fix" the state of a hiding playback sheet and leading to those aforementioned issues. Fix this by band-aiding it with yet a n o t h e r bottom sheet behavior extension that exposes the target state. This will be eventually be used in the whole bottom sheet flow, as it allows me to abort in-progress sheet transitions, but I'm not going to rock the boat like this in a patch release. Probably 3.2.0. Resolves #464. --- CHANGELOG.md | 2 ++ .../bottomsheet/BackportBottomSheetBehavior.java | 13 +++++++++++++ .../main/java/org/oxycblt/auxio/MainFragment.kt | 16 +++++++++------- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8239410b2..122ed0923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ within it - Fixed incorrect songs being displayed when adding albums to the end of the queue - Fixed freezing occuring when scrolling through large music libraries - Fixed app not responding once music loading completes for large libraries +- Fixed crash when the last song of the queue gets removed while playing +- Fixed playback UI not re-appearing after playback ends #### What's Changed - Android Lollipop and Marshmallow support have been dropped diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java index e8c26ff3b..c6560c151 100644 --- a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java +++ b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java @@ -1336,6 +1336,19 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return state; } + /** + * Gets the target state of the bottom sheet if currently attempting to settle, or the current + * state otherwise. + * @return One of {@link #STATE_EXPANDED}, {@link #STATE_HALF_EXPANDED}, {@link #STATE_COLLAPSED}, + * or {@link #STATE_DRAGGING} + */ + public int getTargetState() { + if (state != STATE_SETTLING) { + return state; + } + return stateSettlingTracker.targetState; + } + void setStateInternal(@State int state) { if (this.state == state) { return; diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 4a456100c..7c8d558ac 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -160,7 +160,7 @@ class MainFragment : fillColor = context.getAttrColorCompat(MR.attr.colorSurface) elevation = context.getDimen(R.dimen.elevation_normal) } - // Apply bar insets for the queue's RecyclerView to usee. + // Apply bar insets for the queue's RecyclerView to use. setOnApplyWindowInsetsListener { v, insets -> v.updatePadding(top = insets.systemBarInsetsCompat.top) insets @@ -436,7 +436,7 @@ class MainFragment : val binding = requireBinding() val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior - if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_HIDDEN) { + if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_HIDDEN) { logD("Unhiding and enabling playback sheet") val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? @@ -454,7 +454,7 @@ class MainFragment : val binding = requireBinding() val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior - if (playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) { + if (playbackSheetBehavior.targetState != BackportBottomSheetBehavior.STATE_HIDDEN) { val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? @@ -473,6 +473,8 @@ class MainFragment : } } + // TODO: Use targetState more + private class SheetBackPressedCallback( private val playbackSheetBehavior: PlaybackBottomSheetBehavior<*>, private val queueSheetBehavior: QueueBottomSheetBehavior<*>? @@ -499,13 +501,13 @@ class MainFragment : } private fun playbackSheetShown() = - playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED && - playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN + playbackSheetBehavior.targetState != BackportBottomSheetBehavior.STATE_COLLAPSED && + playbackSheetBehavior.targetState != BackportBottomSheetBehavior.STATE_HIDDEN private fun queueSheetShown() = queueSheetBehavior != null && - queueSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED && - playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED + playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED && + queueSheetBehavior.targetState != BackportBottomSheetBehavior.STATE_COLLAPSED } private class DetailBackPressedCallback(private val detailModel: DetailViewModel) : From e150647573eeb23cf364bd406cbd4b3a15769f07 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 2 Jun 2023 14:45:25 -0600 Subject: [PATCH 37/43] playback: fix improper re-initializaiton Fix issues stemming from how ExoPlayer apparently doesn't send a playWhenReady event after being stopped. This ended up breaking AudioEffect integration and notification posting. I really don't know why player.stop() doesn't do this. --- CHANGELOG.md | 2 +- .../org/oxycblt/auxio/playback/system/PlaybackService.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 122ed0923..4898e1f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ within it - Fixed freezing occuring when scrolling through large music libraries - Fixed app not responding once music loading completes for large libraries - Fixed crash when the last song of the queue gets removed while playing -- Fixed playback UI not re-appearing after playback ends +- Fixed playback UI and notification not re-appearing after playback ends #### What's Changed - Android Lollipop and Marshmallow support have been dropped diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index c1dbbe7c2..ab95fba78 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -226,8 +226,11 @@ class PlaybackService : if (song == null) { // No song, stop playback and foreground state. logD("Nothing playing, stopping playback") + // For some reason the player does not mark playWhenReady as false when stopped, + // which then completely breaks any re-initialization if playback starts again. + // So we manually set it to false here. + player.playWhenReady = false player.stop() - stopAndSave() return } From c59074a4fec7bc9a8c993d9208870550c662529d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 2 Jun 2023 14:54:51 -0600 Subject: [PATCH 38/43] build: bump to 3.1.1 Bump to version 3.1.1 (31). --- CHANGELOG.md | 2 +- README.md | 4 ++-- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/31.txt | 3 +++ 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/31.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4898e1f96..0c2556181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## dev +## 3.1.1 #### What's New - Added ability to share a track diff --git a/README.md b/README.md index 8dd46d94c..f5e5ebabe 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@

Auxio

A simple, rational music player for android.

- - Latest Version + + Latest Version Releases diff --git a/app/build.gradle b/app/build.gradle index 965b4244b..cc1075aad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { defaultConfig { applicationId namespace - versionName "3.1.0" - versionCode 30 + versionName "3.1.1" + versionCode 31 minSdk 24 targetSdk 33 diff --git a/fastlane/metadata/android/en-US/changelogs/31.txt b/fastlane/metadata/android/en-US/changelogs/31.txt new file mode 100644 index 000000000..6b6fd83f1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/31.txt @@ -0,0 +1,3 @@ +Auxio 3.1.0 introduces playlisting functionality, with more features coming soon. +This release fixes major issues identified in the previous version. +For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.1.1. \ No newline at end of file From d786cd16d75eba4188fd221e5cd68d6ed8bd937b Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 2 Jun 2023 17:22:36 -0600 Subject: [PATCH 39/43] all: cleanup Pre-release clean-up. --- .../oxycblt/auxio/music/MusicRepository.kt | 4 --- .../auxio/music/device/DeviceMusicImpl.kt | 21 +++++++++-- .../java/org/oxycblt/auxio/music/info/Date.kt | 35 +++---------------- .../playback/system/NotificationComponent.kt | 13 +------ app/src/main/res/values-iw/strings.xml | 3 -- 5 files changed, 24 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 36133c94c..8d890b218 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -365,10 +365,6 @@ constructor( throw NoAudioPermissionException() } - // TODO: Parallelize this even more aggressively. I can hypothetically connect all - // finalization steps (library, cache, playlists) into a single pipeline would need - // to change how I indicate progress however - // Start initializing the extractors. Use an indeterminate state, as there is no ETA on // how long a media database query will take. emitLoading(IndexingProgress.Indeterminate) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 530f35530..e3ce99928 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -248,8 +248,7 @@ class AlbumImpl( update(rawAlbum.rawArtists.map { it.name }) } override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings) - - override val dates = Date.Range.from(songs.mapNotNull { it.date }) + override val dates: Date.Range? override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null) override val coverUri = rawAlbum.mediaStoreId.toCoverUri() override val durationMs: Long @@ -263,17 +262,35 @@ class AlbumImpl( init { var totalDuration: Long = 0 + var minDate: Date? = null + var maxDate: Date? = null var earliestDateAdded: Long = Long.MAX_VALUE // Do linking and value generation in the same loop for efficiency. for (song in songs) { song.link(this) + + if (song.date != null) { + val min = minDate + if (min == null || song.date < min) { + minDate = song.date + } + + val max = maxDate + if (max == null || song.date > max) { + maxDate = song.date + } + } + if (song.dateAdded < earliestDateAdded) { earliestDateAdded = song.dateAdded } totalDuration += song.durationMs } + val min = minDate + val max = maxDate + dates = if (min != null && max != null) Date.Range(min, max) else null durationMs = totalDuration dateAdded = earliestDateAdded diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt index b47f580db..860b3e315 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt @@ -116,13 +116,15 @@ class Date private constructor(private val tokens: List) : Comparable * * @author Alexander Capehart */ - class Range - private constructor( + class Range( /** The earliest [Date] in the range. */ val min: Date, /** the latest [Date] in the range. May be the same as [min]. ] */ val max: Date ) : Comparable { + init { + check(min <= max) { "Min date must be <= max date" } + } /** * Resolve this instance into a human-readable date range. @@ -145,35 +147,6 @@ class Date private constructor(private val tokens: List) : Comparable override fun hashCode() = 31 * max.hashCode() + min.hashCode() override fun compareTo(other: Range) = min.compareTo(other.min) - - companion object { - /** - * Create a [Range] from the given list of [Date]s. - * - * @param dates The [Date]s to use. - * @return A [Range] based on the minimum and maximum [Date]s. If there are no [Date]s, - * null is returned. - */ - fun from(dates: List): Range? { - if (dates.isEmpty()) { - // Nothing to do. - return null - } - // Simultaneously find the minimum and maximum values in the given range. - // If this list has only one item, then that one date is the minimum and maximum. - var min = dates.first() - var max = min - for (i in 1..dates.lastIndex) { - if (dates[i] < min) { - min = dates[i] - } - if (dates[i] > max) { - max = dates[i] - } - } - return Range(min, max) - } - } } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt index ecbbce122..7b9868072 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback.system import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaSessionCompat import androidx.annotation.DrawableRes @@ -78,17 +77,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes setLargeIcon(metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)) setContentTitle(metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)) setContentText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ARTIST)) - - // Starting in API 24, the subtext field changed semantics from being below the - // content text to being above the title. Use an appropriate field for both. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - // Display description -> Parent in which playback is occurring - logD("API 24+, showing parent information") - setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION)) - } else { - logD("API 24 or lower, showing album information") - setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ALBUM)) - } + setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION)) } /** diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 9b0ef6424..1e1db4996 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -218,7 +218,6 @@ אומן אחד שני אומנים %d אומנים - לכלול רענון מוזיקה @@ -230,13 +229,11 @@ שיר אחד שני שירים %d שירים - אלבום אחד שני אלבומים %d אלבומים - שונה שם לרשימת השמעה רשימת השמעה נמחקה From aae688b642df3636b9b43025ccfb64064d009269 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 3 Jun 2023 09:01:26 -0600 Subject: [PATCH 40/43] home: fix stuck playlist indicator Fix an accidental return statement resulting in the playlist playback indicator not properly updating if a song not in the playlist was played. --- .../java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt index 6a766661a..61fa54b7c 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt @@ -129,7 +129,7 @@ class PlaylistListFragment : private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { // Only highlight the playlist if it is currently playing, and if the currently // playing song is also contained within. - val playlist = (parent as? Playlist)?.takeIf { it.songs.contains(song) } ?: return + val playlist = (parent as? Playlist)?.takeIf { it.songs.contains(song) } playlistAdapter.setPlaying(playlist, isPlaying) } From 182883ef2de7388354253e83615f3668b73ae01f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 3 Jun 2023 09:02:39 -0600 Subject: [PATCH 41/43] music: avoid redundant devicelibrary comparison Avoid redundantly comparing DeviceLibrary instances based on parent information already derived from song instances. --- .../auxio/music/device/DeviceLibrary.kt | 18 +++--------------- .../oxycblt/auxio/music/device/DeviceModule.kt | 2 +- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 96f4915b7..814c15818 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -134,21 +134,9 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } } private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } } - override fun equals(other: Any?) = - other is DeviceLibrary && - other.songs == songs && - other.albums == albums && - other.artists == artists && - other.genres == genres - - override fun hashCode(): Int { - var hashCode = songs.hashCode() - hashCode = hashCode * 31 + albums.hashCode() - hashCode = hashCode * 31 + artists.hashCode() - hashCode = hashCode * 31 + genres.hashCode() - return hashCode - } - + // All other music is built from songs, so comparison only needs to check songs. + override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs + override fun hashCode() = songs.hashCode() override fun toString() = "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, artists=${artists.size}, genres=${genres.size})" diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt index 41b69a498..85e8e511e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt @@ -26,5 +26,5 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface DeviceModule { - @Binds fun deviceLibraryProvider(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory + @Binds fun deviceLibraryFactory(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory } From 736f3ec6b7266c615251eac5debdfa478414d065 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 3 Jun 2023 09:04:44 -0600 Subject: [PATCH 42/43] playback: fix crash on state restore Fix a crash stemming from applying the playback state on the main thread instead of the background thread. all: add misc todos --- app/src/main/java/org/oxycblt/auxio/MainActivity.kt | 2 +- .../oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt | 7 ++++--- .../main/java/org/oxycblt/auxio/music/user/UserLibrary.kt | 1 + .../org/oxycblt/auxio/playback/PlaybackPanelFragment.kt | 2 -- .../org/oxycblt/auxio/playback/system/PlaybackService.kt | 5 ++++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index a5df4f5b4..725f60444 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -51,7 +51,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * TODO: Unit testing * TODO: Fix UID naming * TODO: Leverage FlexibleListAdapter more in dialogs (Disable item anims) - * TODO: Try to move on from synchronized and volatile in shared objs + * TODO: Improve multi-threading support in shared objects */ @AndroidEntryPoint class MainActivity : AppCompatActivity() { diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt index ab0db5fe3..1beddc8ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt @@ -22,6 +22,7 @@ import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW /** * A [RecyclerView.Adapter] that supports indicating the playback status of a particular item. @@ -71,7 +72,7 @@ abstract class PlayingIndicatorAdapter( if (pos > -1) { notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED) } else { - logD("oldItem was not in adapter data") + logW("oldItem was not in adapter data") } } @@ -81,7 +82,7 @@ abstract class PlayingIndicatorAdapter( if (pos > -1) { notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED) } else { - logD("newItem was not in adapter data") + logW("newItem was not in adapter data") } } @@ -99,7 +100,7 @@ abstract class PlayingIndicatorAdapter( if (pos > -1) { notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED) } else { - logD("newItem was not in adapter data") + logW("newItem was not in adapter data") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index f6a888eb7..412b14fa4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -161,6 +161,7 @@ private class UserLibraryImpl( override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name } override suspend fun createPlaylist(name: String, songs: List) { + // TODO: Use synchronized with value access too val playlistImpl = PlaylistImpl.from(name, songs, musicSettings) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } val rawPlaylist = diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index abd7aafbc..6bcc4114e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -236,13 +236,11 @@ class PlaybackPanelFragment : requireBinding().playbackShuffle.isActivated = isShuffled } - /** Navigate to one of the currently playing [Song]'s Artists. */ private fun navigateToCurrentArtist() { val song = playbackModel.song.value ?: return navModel.exploreNavigateToParentArtist(song) } - /** Navigate to the currently playing [Song]'s albums. */ private fun navigateToCurrentAlbum() { val song = playbackModel.song.value ?: return navModel.exploreNavigateTo(song.album) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index ab95fba78..848d47b4d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings @@ -355,7 +356,9 @@ class PlaybackService : logD("Restoring playback state") restoreScope.launch { persistenceRepository.readState()?.let { - playbackManager.applySavedState(it, false) + // Apply the saved state on the main thread to prevent code expecting + // state updates on the main thread from crashing. + withContext(Dispatchers.Main) { playbackManager.applySavedState(it, false) } } } } From d49034b66416aad33214321b91e08a2f20a1dc13 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 3 Jun 2023 09:24:10 -0600 Subject: [PATCH 43/43] ui: fix back listener leak Fix an issue where the OnBackPressedListeners would leak their bound views once MainFragment's view was destroyed. --- .../java/org/oxycblt/auxio/MainFragment.kt | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 7c8d558ac..ba09eddf0 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -81,10 +81,10 @@ class MainFragment : private val playbackModel: PlaybackViewModel by activityViewModels() private val selectionModel: SelectionViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() - private lateinit var sheetBackCallback: SheetBackPressedCallback - private lateinit var detailBackCallback: DetailBackPressedCallback - private lateinit var selectionBackCallback: SelectionBackPressedCallback - private lateinit var exploreBackCallback: ExploreBackPressedCallback + private var sheetBackCallback: SheetBackPressedCallback? = null + private var detailBackCallback: DetailBackPressedCallback? = null + private var selectionBackCallback: SelectionBackPressedCallback? = null + private var exploreBackCallback: ExploreBackPressedCallback? = null private var lastInsets: WindowInsets? = null private var elevationNormal = 0f private var initialNavDestinationChange = true @@ -110,13 +110,17 @@ class MainFragment : // Currently all back press callbacks are handled in MainFragment, as it's not guaranteed // that instantiating these callbacks in their respective fragments would result in the // correct order. - sheetBackCallback = + val sheetBackCallback = SheetBackPressedCallback( - playbackSheetBehavior = playbackSheetBehavior, - queueSheetBehavior = queueSheetBehavior) - detailBackCallback = DetailBackPressedCallback(detailModel) - selectionBackCallback = SelectionBackPressedCallback(selectionModel) - exploreBackCallback = ExploreBackPressedCallback(binding.exploreNavHost) + playbackSheetBehavior = playbackSheetBehavior, + queueSheetBehavior = queueSheetBehavior) + .also { sheetBackCallback = it } + val detailBackCallback = + DetailBackPressedCallback(detailModel).also { detailBackCallback = it } + val selectionBackCallback = + SelectionBackPressedCallback(selectionModel).also { selectionBackCallback = it } + val exploreBackCallback = + ExploreBackPressedCallback(binding.exploreNavHost).also { exploreBackCallback = it } // --- UI SETUP --- val context = requireActivity() @@ -202,6 +206,14 @@ class MainFragment : binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this) } + override fun onDestroyBinding(binding: FragmentMainBinding) { + super.onDestroyBinding(binding) + sheetBackCallback = null + detailBackCallback = null + selectionBackCallback = null + exploreBackCallback = null + } + override fun onPreDraw(): Boolean { // We overload CoordinatorLayout far too much to rely on any of it's typical // listener functionality. Just update all transitions before every draw. Should @@ -287,7 +299,8 @@ class MainFragment : // Since the navigation listener is also reliant on the bottom sheets, we must also update // it every frame. - sheetBackCallback.invalidateEnabled() + requireNotNull(sheetBackCallback) { "SheetBackPressedCallback was not available" } + .invalidateEnabled() return true } @@ -300,7 +313,8 @@ class MainFragment : // Drop the initial call by NavController that simply provides us with the current // destination. This would cause the selection state to be lost every time the device // rotates. - exploreBackCallback.invalidateEnabled() + requireNotNull(exploreBackCallback) { "ExploreBackPressedCallback was not available" } + .invalidateEnabled() if (!initialNavDestinationChange) { initialNavDestinationChange = true return