From 699227c1a8a840f0d12e171fa6f4744d6017bd51 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 26 May 2023 16:26:31 -0600 Subject: [PATCH] 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