all: relog project

Fill in a lot of code paths in the project with log statements in order
to improve the debugging experience.
This commit is contained in:
Alexander Capehart 2023-05-26 16:26:31 -06:00
parent b037cfb166
commit 699227c1a8
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
106 changed files with 1068 additions and 346 deletions

View file

@ -36,6 +36,7 @@ import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
@ -121,6 +122,7 @@ class MainActivity : AppCompatActivity() {
private fun startIntentAction(intent: Intent?): Boolean { private fun startIntentAction(intent: Intent?): Boolean {
if (intent == null) { if (intent == null) {
// Nothing to do. // Nothing to do.
logD("No intent to handle")
return false return false
} }
@ -129,6 +131,7 @@ class MainActivity : AppCompatActivity() {
// This is because onStart can run multiple times, and thus we really don't // 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 // want to return false and override the original delayed action with a
// RestoreState action. // RestoreState action.
logD("Already used this intent")
return true return true
} }
intent.putExtra(KEY_INTENT_USED, true) intent.putExtra(KEY_INTENT_USED, true)
@ -137,8 +140,12 @@ class MainActivity : AppCompatActivity() {
when (intent.action) { when (intent.action) {
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false) Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll 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) playbackModel.startAction(action)
return true return true
} }

View file

@ -57,6 +57,7 @@ import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.coordinatorLayoutBehavior
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -66,6 +67,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* high-level navigation features. * high-level navigation features.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*
* TODO: Break up the god navigation setup going on here
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class MainFragment : class MainFragment :
@ -115,9 +118,11 @@ class MainFragment :
val queueSheetBehavior = val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
if (queueSheetBehavior != null) { 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 = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
// TODO: Use the material handle
unlikelyToBeNull(binding.handleWrapper).setOnClickListener { unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED && if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) { queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
@ -127,6 +132,7 @@ class MainFragment :
} }
} else { } else {
// Dual-pane mode, manually style the static queue sheet. // Dual-pane mode, manually style the static queue sheet.
logD("Configuring dual-pane bottom sheet")
binding.queueSheet.apply { binding.queueSheet.apply {
// Emulate the elevated bottom sheet style. // Emulate the elevated bottom sheet style.
background = background =
@ -280,19 +286,15 @@ class MainFragment :
} }
private fun handleMainNavigation(action: MainNavigationAction?) { private fun handleMainNavigation(action: MainNavigationAction?) {
if (action == null) { if (action != null) {
// Nothing to do. when (action) {
return 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?) { private fun handleExploreNavigation(item: Music?) {
@ -377,6 +379,7 @@ class MainFragment :
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) { if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is not expanded and not hidden, we can expand it. // Playback sheet is not expanded and not hidden, we can expand it.
logD("Expanding playback sheet")
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED
return return
} }
@ -387,6 +390,7 @@ class MainFragment :
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
// Queue sheet and playback sheet is expanded, close the queue sheet so the // Queue sheet and playback sheet is expanded, close the queue sheet so the
// playback panel can eb shown. // playback panel can eb shown.
logD("Collapsing queue sheet")
queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
} }
} }
@ -397,6 +401,7 @@ class MainFragment :
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
// Playback sheet (and possibly queue) needs to be collapsed. // Playback sheet (and possibly queue) needs to be collapsed.
logD("Closing playback and queue sheets")
val queueSheetBehavior = val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
@ -409,6 +414,7 @@ class MainFragment :
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_HIDDEN) { if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_HIDDEN) {
logD("Unhiding and enabling playback sheet")
val queueSheetBehavior = val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
// Queue sheet behavior is either collapsed or expanded, no hiding needed // Queue sheet behavior is either collapsed or expanded, no hiding needed
@ -429,6 +435,8 @@ class MainFragment :
val queueSheetBehavior = val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? 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. // Make both bottom sheets non-draggable so the user can't halt the hiding event.
queueSheetBehavior?.apply { queueSheetBehavior?.apply {
isDraggable = false isDraggable = false
@ -458,6 +466,7 @@ class MainFragment :
if (queueSheetBehavior != null && if (queueSheetBehavior != null &&
queueSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED && queueSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) { playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
logD("Hiding queue sheet")
queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
return return
} }
@ -465,21 +474,25 @@ class MainFragment :
// If expanded, collapse the playback sheet next. // If expanded, collapse the playback sheet next.
if (playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED && if (playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) { playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) {
logD("Hiding playback sheet")
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
return return
} }
// Clear out pending playlist edits. // Clear out pending playlist edits.
if (detailModel.dropPlaylistEdit()) { if (detailModel.dropPlaylistEdit()) {
logD("Dropping playlist edits")
return return
} }
// Clear out any prior selections. // Clear out any prior selections.
if (selectionModel.drop()) { if (selectionModel.drop()) {
logD("Dropping selection")
return return
} }
// Then try to navigate out of the explore navigation fragments (i.e Detail Views) // Then try to navigate out of the explore navigation fragments (i.e Detail Views)
logD("Navigate away from explore view")
binding.exploreNavHost.findNavController().navigateUp() binding.exploreNavHost.findNavController().navigateUp()
} }
@ -500,6 +513,10 @@ class MainFragment :
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
val exploreNavController = binding.exploreNavHost.findNavController() 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 = isEnabled =
queueSheetBehavior?.state == BackportBottomSheetBehavior.STATE_EXPANDED || queueSheetBehavior?.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED || playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED ||

View file

@ -55,6 +55,7 @@ import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.share
@ -168,7 +169,10 @@ class AlbumDetailFragment :
requireContext().share(currentAlbum) requireContext().share(currentAlbum)
true true
} }
else -> false else -> {
logW("Unexpected menu item selected")
false
}
} }
} }
@ -222,7 +226,7 @@ class AlbumDetailFragment :
private fun updateAlbum(album: Album?) { private fun updateAlbum(album: Album?) {
if (album == null) { if (album == null) {
// Album we were showing no longer exists. logD("No album to show, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
return return
} }
@ -231,12 +235,8 @@ class AlbumDetailFragment :
} }
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) { albumListAdapter.setPlaying(
albumListAdapter.setPlaying(song, isPlaying) song.takeIf { parent == detailModel.currentAlbum.value }, isPlaying)
} else {
// Clear the ViewHolders if the mode isn't ALL_SONGS
albumListAdapter.setPlaying(null, isPlaying)
}
} }
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
@ -303,7 +303,7 @@ class AlbumDetailFragment :
boxStart: Int, boxStart: Int,
boxEnd: Int, boxEnd: Int,
snapPreference: Int snapPreference: Int
): Int = ) =
(boxStart + (boxEnd - boxStart) / 2) - (boxStart + (boxEnd - boxStart) / 2) -
(viewStart + (viewEnd - viewStart) / 2) (viewStart + (viewEnd - viewStart) / 2)
} }

View file

@ -52,6 +52,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.share
@ -164,7 +165,10 @@ class ArtistDetailFragment :
requireContext().share(currentArtist) requireContext().share(currentArtist)
true true
} }
else -> false else -> {
logW("Unexpected menu item selected")
false
}
} }
} }
@ -233,7 +237,7 @@ class ArtistDetailFragment :
private fun updateArtist(artist: Artist?) { private fun updateArtist(artist: Artist?) {
if (artist == null) { if (artist == null) {
// Artist we were showing no longer exists. logD("No artist to show, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
return return
} }
@ -242,6 +246,9 @@ class ArtistDetailFragment :
// Disable options that make no sense with an empty artist // Disable options that make no sense with an empty artist
val playable = artist.songs.isNotEmpty() 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_play_next).isEnabled = playable
menu.findItem(R.id.action_queue_add).isEnabled = playable menu.findItem(R.id.action_queue_add).isEnabled = playable
menu.findItem(R.id.action_playlist_add).isEnabled = playable menu.findItem(R.id.action_playlist_add).isEnabled = playable

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.CoordinatorAppBarLayout import org.oxycblt.auxio.ui.CoordinatorAppBarLayout
import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.lazyReflectedField 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 * 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 { (TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply {
// We can never properly initialize the title view's state before draw time, // 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 // so we just set it's alpha to 0f to produce a less jarring initialization
// animation.. // animation.
alpha = 0f alpha = 0f
} }
@ -101,12 +102,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (titleShown == visible) return if (titleShown == visible) return
titleShown = visible 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 // Emulate the AppBarLayout lift animation (Linear, alpha 0f -> 1f), but now with
// the title view's alpha instead of the AppBarLayout's elevation. // the title view's alpha instead of the AppBarLayout's elevation.
val titleView = findTitleView() val titleView = findTitleView()
@ -126,7 +121,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
return return
} }
this.titleAnimator = logD("Changing title visibility [from: $from to: $to]")
titleAnimator?.cancel()
titleAnimator =
ValueAnimator.ofFloat(from, to).apply { ValueAnimator.ofFloat(from, to).apply {
addUpdateListener { titleView.alpha = it.animatedValue as Float } addUpdateListener { titleView.alpha = it.animatedValue as Float }
duration = duration =

View file

@ -53,6 +53,7 @@ import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.MutableEvent
import org.oxycblt.auxio.util.logD 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 * [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) { if (changes.userLibrary && userLibrary != null) {
val playlist = currentPlaylist.value val playlist = currentPlaylist.value
if (playlist != null) { if (playlist != null) {
logD("Updated playlist to ${currentPlaylist.value}")
_currentPlaylist.value = _currentPlaylist.value =
userLibrary.findPlaylist(playlist.uid)?.also(::refreshPlaylistList) 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. * @param uid The UID of the [Song] to load. Must be valid.
*/ */
fun setSong(uid: Music.UID) { fun setSong(uid: Music.UID) {
logD("Opening Song [uid: $uid]") logD("Opening song $uid")
_currentSong.value = musicRepository.deviceLibrary?.findSong(uid)?.also(::refreshAudioInfo) _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. * @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid.
*/ */
fun setAlbum(uid: Music.UID) { fun setAlbum(uid: Music.UID) {
logD("Opening Album [uid: $uid]") logD("Opening album $uid")
_currentAlbum.value = _currentAlbum.value =
musicRepository.deviceLibrary?.findAlbum(uid)?.also(::refreshAlbumList) 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. * @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid.
*/ */
fun setArtist(uid: Music.UID) { fun setArtist(uid: Music.UID) {
logD("Opening Artist [uid: $uid]") logD("Opening artist $uid")
_currentArtist.value = _currentArtist.value =
musicRepository.deviceLibrary?.findArtist(uid)?.also(::refreshArtistList) 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. * @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid.
*/ */
fun setGenre(uid: Music.UID) { fun setGenre(uid: Music.UID) {
logD("Opening Genre [uid: $uid]") logD("Opening genre $uid")
_currentGenre.value = _currentGenre.value =
musicRepository.deviceLibrary?.findGenre(uid)?.also(::refreshGenreList) 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. * @param uid The [Music.UID] of the [Playlist] to update [currentPlaylist] to. Must be valid.
*/ */
fun setPlaylist(uid: Music.UID) { fun setPlaylist(uid: Music.UID) {
logD("Opening Playlist [uid: $uid]") logD("Opening playlist $uid")
_currentPlaylist.value = _currentPlaylist.value =
musicRepository.userLibrary?.findPlaylist(uid)?.also(::refreshPlaylistList) 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. */ /** Start a playlist editing session. Does nothing if a playlist is not being shown. */
@ -310,6 +326,7 @@ constructor(
fun savePlaylistEdit() { fun savePlaylistEdit() {
val playlist = _currentPlaylist.value ?: return val playlist = _currentPlaylist.value ?: return
val editedPlaylist = _editedPlaylist.value ?: return val editedPlaylist = _editedPlaylist.value ?: return
logD("Committing playlist edits")
viewModelScope.launch { viewModelScope.launch {
musicRepository.rewritePlaylist(playlist, editedPlaylist) musicRepository.rewritePlaylist(playlist, editedPlaylist)
// TODO: The user could probably press some kind of button if they were fast enough. // TODO: The user could probably press some kind of button if they were fast enough.
@ -330,6 +347,7 @@ constructor(
// Nothing to do. // Nothing to do.
return false return false
} }
logD("Discarding playlist edits")
_editedPlaylist.value = null _editedPlaylist.value = null
refreshPlaylistList(playlist) refreshPlaylistList(playlist)
return true return true
@ -351,6 +369,7 @@ constructor(
if (realFrom !in editedPlaylist.indices || realTo !in editedPlaylist.indices) { if (realFrom !in editedPlaylist.indices || realTo !in editedPlaylist.indices) {
return false return false
} }
logD("Moving playlist song from $realFrom [$from] to $realTo [$to]")
editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo)) editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo))
_editedPlaylist.value = editedPlaylist _editedPlaylist.value = editedPlaylist
refreshPlaylistList(playlist, UpdateInstructions.Move(from, to)) refreshPlaylistList(playlist, UpdateInstructions.Move(from, to))
@ -369,6 +388,7 @@ constructor(
if (realAt !in editedPlaylist.indices) { if (realAt !in editedPlaylist.indices) {
return return
} }
logD("Removing playlist song at $realAt [$at]")
editedPlaylist.removeAt(realAt) editedPlaylist.removeAt(realAt)
_editedPlaylist.value = editedPlaylist _editedPlaylist.value = editedPlaylist
refreshPlaylistList( refreshPlaylistList(
@ -376,11 +396,13 @@ constructor(
if (editedPlaylist.isNotEmpty()) { if (editedPlaylist.isNotEmpty()) {
UpdateInstructions.Remove(at, 1) UpdateInstructions.Remove(at, 1)
} else { } else {
logD("Playlist will be empty after removal, removing header")
UpdateInstructions.Remove(at - 2, 3) UpdateInstructions.Remove(at - 2, 3)
}) })
} }
private fun refreshAudioInfo(song: Song) { private fun refreshAudioInfo(song: Song) {
logD("Refreshing audio info")
// Clear any previous job in order to avoid stale data from appearing in the UI. // Clear any previous job in order to avoid stale data from appearing in the UI.
currentSongJob?.cancel() currentSongJob?.cancel()
_songAudioProperties.value = null _songAudioProperties.value = null
@ -388,6 +410,7 @@ constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val info = audioPropertiesFactory.extract(song) val info = audioPropertiesFactory.extract(song)
yield() yield()
logD("Updating audio info to $info")
_songAudioProperties.value = info _songAudioProperties.value = info
} }
} }
@ -421,6 +444,7 @@ constructor(
list.addAll(songs) list.addAll(songs)
} }
logD("Update album list to ${list.size} items with $instructions")
_albumInstructions.put(instructions) _albumInstructions.put(instructions)
_albumList.value = list _albumList.value = list
} }
@ -454,6 +478,7 @@ constructor(
// groupByTo normally returns a mapping to a MutableList mapping. Since MutableList // 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 // inherits list, we can cast upwards and save a copy by directly inserting the
// implicit album list into the mapping. // implicit album list into the mapping.
logD("Implicit albums present, adding to list")
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(grouping as MutableMap<AlbumGrouping, List<Album>>)[AlbumGrouping.APPEARANCES] = (grouping as MutableMap<AlbumGrouping, List<Album>>)[AlbumGrouping.APPEARANCES] =
artist.implicitAlbums artist.implicitAlbums
@ -482,6 +507,7 @@ constructor(
list.addAll(artistSongSort.songs(artist.songs)) list.addAll(artistSongSort.songs(artist.songs))
} }
logD("Updating artist list to ${list.size} items with $instructions")
_artistInstructions.put(instructions) _artistInstructions.put(instructions)
_artistList.value = list.toList() _artistList.value = list.toList()
} }
@ -500,12 +526,14 @@ constructor(
list.add(songHeader) list.add(songHeader)
val instructions = val instructions =
if (replace) { 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) UpdateInstructions.Replace(list.size)
} else { } else {
UpdateInstructions.Diff UpdateInstructions.Diff
} }
list.addAll(genreSongSort.songs(genre.songs)) list.addAll(genreSongSort.songs(genre.songs))
logD("Updating genre list to ${list.size} items with $instructions")
_genreInstructions.put(instructions) _genreInstructions.put(instructions)
_genreList.value = list _genreList.value = list
} }
@ -525,6 +553,7 @@ constructor(
list.addAll(songs) list.addAll(songs)
} }
logD("Updating playlist list to ${list.size} items with $instructions")
_playlistInstructions.put(instructions) _playlistInstructions.put(instructions)
_playlistList.value = list _playlistList.value = list
} }

View file

@ -53,6 +53,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.share
@ -163,7 +164,10 @@ class GenreDetailFragment :
requireContext().share(currentGenre) requireContext().share(currentGenre)
true true
} }
else -> false else -> {
logW("Unexpected menu item selected")
false
}
} }
} }
@ -230,7 +234,7 @@ class GenreDetailFragment :
private fun updatePlaylist(genre: Genre?) { private fun updatePlaylist(genre: Genre?) {
if (genre == null) { if (genre == null) {
// Genre we were showing no longer exists. logD("No genre to show, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
return return
} }

View file

@ -56,6 +56,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.setFullWidthLookup
import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.share
@ -218,7 +219,10 @@ class PlaylistDetailFragment :
detailModel.savePlaylistEdit() detailModel.savePlaylistEdit()
true true
} }
else -> false else -> {
logW("Unexpected menu item selected")
false
}
} }
} }
@ -259,6 +263,9 @@ class PlaylistDetailFragment :
title = playlist.name.resolve(requireContext()) title = playlist.name.resolve(requireContext())
// Disable options that make no sense with an empty playlist // Disable options that make no sense with an empty playlist
val playable = playlist.songs.isNotEmpty() 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_play_next).isEnabled = playable
menu.findItem(R.id.action_queue_add).isEnabled = playable menu.findItem(R.id.action_queue_add).isEnabled = playable
menu.findItem(R.id.action_share).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) { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
// Prefer songs that might be playing from this playlist. // Prefer songs that are playing from this playlist.
if (parent is Playlist && playlistListAdapter.setPlaying(
parent.uid == unlikelyToBeNull(detailModel.currentPlaylist.value).uid) { song.takeIf { parent == detailModel.currentPlaylist.value }, isPlaying)
playlistListAdapter.setPlaying(song, isPlaying)
} else {
playlistListAdapter.setPlaying(null, isPlaying)
}
} }
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
@ -312,6 +315,7 @@ class PlaylistDetailFragment :
selectionModel.drop() selectionModel.drop()
if (editedPlaylist != null) { if (editedPlaylist != null) {
logD("Updating save button state")
requireBinding().detailEditToolbar.menu.findItem(R.id.action_save).apply { requireBinding().detailEditToolbar.menu.findItem(R.id.action_save).apply {
isEnabled = editedPlaylist != detailModel.currentPlaylist.value?.songs isEnabled = editedPlaylist != detailModel.currentPlaylist.value?.songs
} }
@ -333,9 +337,18 @@ class PlaylistDetailFragment :
private fun updateMultiToolbar() { private fun updateMultiToolbar() {
val id = val id =
when { when {
detailModel.editedPlaylist.value != null -> R.id.detail_edit_toolbar detailModel.editedPlaylist.value != null -> {
selectionModel.selected.value.isNotEmpty() -> R.id.detail_selection_toolbar logD("Currently editing playlist, showing edit toolbar")
else -> R.id.detail_normal_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) requireBinding().detailToolbar.setVisible(id)

View file

@ -41,6 +41,7 @@ import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.concatLocalized
import org.oxycblt.auxio.util.logD
/** /**
* A [ViewBindingDialogFragment] that shows information about a Song. * A [ViewBindingDialogFragment] that shows information about a Song.
@ -73,7 +74,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
private fun updateSong(song: Song?, info: AudioProperties?) { private fun updateSong(song: Song?, info: AudioProperties?) {
if (song == null) { if (song == null) {
// Song we were showing no longer exists. logD("No song to show, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
return return
} }
@ -86,7 +87,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
add(SongProperty(R.string.lbl_album, song.album.zipName(context))) 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_artists, song.artists.zipNames(context)))
add(SongProperty(R.string.lbl_genres, song.genres.resolveNames(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 { song.track?.let {
add(SongProperty(R.string.lbl_track, getString(R.string.fmt_number, it))) add(SongProperty(R.string.lbl_track, getString(R.string.fmt_number, it)))
} }

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.detail.header
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.util.logD
/** /**
* A [RecyclerView.Adapter] that implements shared behavior between each parent header view. * A [RecyclerView.Adapter] that implements shared behavior between each parent header view.
@ -47,6 +48,7 @@ abstract class DetailHeaderAdapter<T : MusicParent, VH : RecyclerView.ViewHolder
* @param parent The new [MusicParent] to show. * @param parent The new [MusicParent] to show.
*/ */
fun setParent(parent: T) { fun setParent(parent: T) {
logD("Updating parent [old: $currentParent new: $parent]")
currentParent = parent currentParent = parent
rebindParent() rebindParent()
} }
@ -55,6 +57,7 @@ abstract class DetailHeaderAdapter<T : MusicParent, VH : RecyclerView.ViewHolder
* Forces the parent [RecyclerView.ViewHolder] to rebind as soon as possible, with no animation. * Forces the parent [RecyclerView.ViewHolder] to rebind as soon as possible, with no animation.
*/ */
protected fun rebindParent() { protected fun rebindParent() {
logD("Rebinding parent")
notifyItemChanged(0, PAYLOAD_UPDATE_HEADER) notifyItemChanged(0, PAYLOAD_UPDATE_HEADER)
} }

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/** /**
* A [DetailHeaderAdapter] that shows [Playlist] information. * A [DetailHeaderAdapter] that shows [Playlist] information.
@ -57,6 +58,7 @@ class PlaylistDetailHeaderAdapter(private val listener: Listener) :
// Nothing to do. // Nothing to do.
return return
} }
logD("Updating editing state [old: ${editedPlaylist?.size} new: ${songs?.size}")
editedPlaylist = songs editedPlaylist = songs
rebindParent() rebindParent()
} }
@ -102,12 +104,17 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
binding.context.getString(R.string.def_song_count) binding.context.getString(R.string.def_song_count)
} }
val playable = playlist.songs.isNotEmpty() && editedPlaylist == null
if (!playable) {
logD("Playlist is being edited or is empty, disabling playback options")
}
binding.detailPlayButton.apply { binding.detailPlayButton.apply {
isEnabled = playlist.songs.isNotEmpty() && editedPlaylist == null isEnabled = playable
setOnClickListener { listener.onPlay() } setOnClickListener { listener.onPlay() }
} }
binding.detailShuffleButton.apply { binding.detailShuffleButton.apply {
isEnabled = playlist.songs.isNotEmpty() && editedPlaylist == null isEnabled = playable
setOnClickListener { listener.onShuffle() } setOnClickListener { listener.onShuffle() }
} }
} }

View file

@ -37,6 +37,7 @@ import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/** /**
* An [DetailListAdapter] implementing the header and sub-items for the [Album] detail view. * An [DetailListAdapter] implementing the header and sub-items for the [Album] detail view.
@ -116,6 +117,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
isGone = text == null isGone = text == null
} }
} else { } else {
logD("Disc is null, defaulting to no disc")
binding.discNumber.text = binding.context.getString(R.string.def_disc) binding.discNumber.text = binding.context.getString(R.string.def_disc)
binding.discName.isGone = true binding.discName.isGone = true
} }

View file

@ -26,7 +26,6 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.list.DetailListAdapter.Listener
import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header

View file

@ -48,6 +48,7 @@ import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/** /**
* A [DetailListAdapter] implementing the header, sub-items, and editing state for the [Playlist] * A [DetailListAdapter] implementing the header, sub-items, and editing state for the [Playlist]
@ -98,6 +99,7 @@ class PlaylistDetailListAdapter(private val listener: Listener) :
// Nothing to do. // Nothing to do.
return return
} }
logD("Updating editing state [old: $isEditing new: $editing]")
this.isEditing = editing this.isEditing = editing
notifyItemRangeChanged(1, currentList.size - 1, PAYLOAD_EDITING_CHANGED) notifyItemRangeChanged(1, currentList.size - 1, PAYLOAD_EDITING_CHANGED)
} }

View file

@ -44,23 +44,32 @@ constructor(
override fun show() { override fun show() {
// Will already show eventually, need to do nothing. // Will already show eventually, need to do nothing.
if (flipping) return if (flipping) {
logD("Already flipping, aborting show")
return
}
// Apply the new configuration possibly set in flipTo. This should occur even if // Apply the new configuration possibly set in flipTo. This should occur even if
// a flip was canceled by a hide. // a flip was canceled by a hide.
pendingConfig?.run { pendingConfig?.run {
this@FlipFloatingActionButton.logD("Applying pending configuration")
setImageResource(iconRes) setImageResource(iconRes)
contentDescription = context.getString(contentDescriptionRes) contentDescription = context.getString(contentDescriptionRes)
setOnClickListener(clickListener) setOnClickListener(clickListener)
} }
pendingConfig = null pendingConfig = null
logD("Beginning show")
super.show() super.show()
} }
override fun hide() { override fun hide() {
if (flipping) {
logD("Hide was called, aborting flip")
}
// Not flipping anymore, disable the flag so that the FAB is not re-shown. // Not flipping anymore, disable the flag so that the FAB is not re-shown.
flipping = false flipping = false
// Don't pass any kind of listener so that future flip operations will not be able // Don't pass any kind of listener so that future flip operations will not be able
// to show the FAB again. // to show the FAB again.
logD("Beginning hide")
super.hide() super.hide()
} }
@ -82,9 +91,12 @@ constructor(
// Already hiding for whatever reason, apply the configuration when the FAB is shown again. // Already hiding for whatever reason, apply the configuration when the FAB is shown again.
if (!isOrWillBeHidden) { if (!isOrWillBeHidden) {
logD("Starting hide for flip")
flipping = true flipping = true
// We will re-show the FAB later, assuming that there was not a prior flip operation. // We will re-show the FAB later, assuming that there was not a prior flip operation.
super.hide(FlipVisibilityListener()) super.hide(FlipVisibilityListener())
} else {
logD("Already hiding, will apply config later")
} }
} }
@ -97,7 +109,7 @@ constructor(
private inner class FlipVisibilityListener : OnVisibilityChangedListener() { private inner class FlipVisibilityListener : OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton) { override fun onHidden(fab: FloatingActionButton) {
if (!flipping) return if (!flipping) return
logD("Showing for a flip operation") logD("Starting show for flip")
flipping = false flipping = false
show() show()
} }

View file

@ -76,6 +76,7 @@ import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -210,54 +211,65 @@ class HomeFragment :
return true return true
} }
when (item.itemId) { return when (item.itemId) {
// Handle main actions (Search, Settings, About) // Handle main actions (Search, Settings, About)
R.id.action_search -> { R.id.action_search -> {
logD("Navigating to search") logD("Navigating to search")
setupAxisTransitions(MaterialSharedAxis.Z) setupAxisTransitions(MaterialSharedAxis.Z)
findNavController().navigateSafe(HomeFragmentDirections.actionShowSearch()) findNavController().navigateSafe(HomeFragmentDirections.actionShowSearch())
true
} }
R.id.action_settings -> { R.id.action_settings -> {
logD("Navigating to settings") logD("Navigating to settings")
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings())) MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings()))
true
} }
R.id.action_about -> { R.id.action_about -> {
logD("Navigating to about") logD("Navigating to about")
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout())) MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout()))
true
} }
// Handle sort menu // Handle sort menu
R.id.submenu_sorting -> { R.id.submenu_sorting -> {
// Junk click event when opening the menu // Junk click event when opening the menu
true
} }
R.id.option_sort_asc -> { R.id.option_sort_asc -> {
logD("Switching to ascending sorting")
item.isChecked = true item.isChecked = true
homeModel.setSortForCurrentTab( homeModel.setSortForCurrentTab(
homeModel homeModel
.getSortForTab(homeModel.currentTabMode.value) .getSortForTab(homeModel.currentTabMode.value)
.withDirection(Sort.Direction.ASCENDING)) .withDirection(Sort.Direction.ASCENDING))
true
} }
R.id.option_sort_dec -> { R.id.option_sort_dec -> {
logD("Switching to descending sorting")
item.isChecked = true item.isChecked = true
homeModel.setSortForCurrentTab( homeModel.setSortForCurrentTab(
homeModel homeModel
.getSortForTab(homeModel.currentTabMode.value) .getSortForTab(homeModel.currentTabMode.value)
.withDirection(Sort.Direction.DESCENDING)) .withDirection(Sort.Direction.DESCENDING))
true
} }
else -> { else -> {
// Sorting option was selected, mark it as selected and update the mode val newMode = Sort.Mode.fromItemId(item.itemId)
item.isChecked = true if (newMode != null) {
homeModel.setSortForCurrentTab( // Sorting option was selected, mark it as selected and update the mode
homeModel logD("Updating sort mode")
.getSortForTab(homeModel.currentTabMode.value) item.isChecked = true
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))) 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) { private fun setupPager(binding: FragmentHomeBinding) {
@ -268,6 +280,7 @@ class HomeFragment :
if (homeModel.currentTabModes.size == 1) { if (homeModel.currentTabModes.size == 1) {
// A single tab makes the tab layout redundant, hide it and disable the collapsing // A single tab makes the tab layout redundant, hide it and disable the collapsing
// behavior. // behavior.
logD("Single tab shown, disabling TabLayout")
binding.homeTabs.isVisible = false binding.homeTabs.isVisible = false
binding.homeAppbar.setExpanded(true, false) binding.homeAppbar.setExpanded(true, false)
toolbarParams.scrollFlags = 0 toolbarParams.scrollFlags = 0
@ -292,17 +305,26 @@ class HomeFragment :
val isVisible: (Int) -> Boolean = val isVisible: (Int) -> Boolean =
when (tabMode) { when (tabMode) {
// Disallow sorting by count for songs // 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 // 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 // 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_asc ||
id == R.id.option_sort_dec || id == R.id.option_sort_dec ||
id == R.id.option_sort_name || id == R.id.option_sort_name ||
id == R.id.option_sort_count || id == R.id.option_sort_count ||
id == R.id.option_sort_duration id == R.id.option_sort_duration
} })
}
} }
val sortMenu = val sortMenu =
@ -310,18 +332,29 @@ class HomeFragment :
val toHighlight = homeModel.getSortForTab(tabMode) val toHighlight = homeModel.getSortForTab(tabMode)
for (option in sortMenu) { 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. // the current sort of the tab.
if (option.itemId == toHighlight.mode.itemId || if (isCurrentMode || isCurrentlyAscending || isCurrentlyDescending) {
(option.itemId == R.id.option_sort_asc && logD(
toHighlight.direction == Sort.Direction.ASCENDING) || "Checking $option [mode: $isCurrentMode asc: $$isCurrentlyAscending dec: $isCurrentlyDescending]")
(option.itemId == R.id.option_sort_dec && // Note: We cannot inline this boolean assignment since it unchecks all other radio
toHighlight.direction == Sort.Direction.DESCENDING)) { // buttons (even when setting it to false), which would result in nothing being
// selected.
option.isChecked = true option.isChecked = true
} }
// Disable options that are not allowed by the isVisible lambda // Disable options that are not allowed by the isVisible lambda
option.isVisible = isVisible(option.itemId) option.isVisible = isVisible(option.itemId)
if (!option.isVisible) {
logD("Hiding $option")
}
} }
// Update the scrolling view in AppBarLayout to align with the current tab's // Update the scrolling view in AppBarLayout to align with the current tab's
@ -337,10 +370,12 @@ class HomeFragment :
} }
if (tabMode != MusicMode.PLAYLISTS) { if (tabMode != MusicMode.PLAYLISTS) {
logD("Flipping to shuffle button")
binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) { binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) {
playbackModel.shuffleAll() playbackModel.shuffleAll()
} }
} else { } else {
logD("Flipping to playlist button")
binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) { binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) {
musicModel.createPlaylist() musicModel.createPlaylist()
} }
@ -350,6 +385,7 @@ class HomeFragment :
private fun handleRecreate(recreate: Unit?) { private fun handleRecreate(recreate: Unit?) {
if (recreate == null) return if (recreate == null) return
val binding = requireBinding() val binding = requireBinding()
logD("Recreating ViewPager")
// Move back to position zero, as there must be a tab there. // Move back to position zero, as there must be a tab there.
binding.homePager.currentItem = 0 binding.homePager.currentItem = 0
// Make sure tabs are set up to also follow the new ViewPager configuration. // Make sure tabs are set up to also follow the new ViewPager configuration.
@ -386,7 +422,7 @@ class HomeFragment :
binding.homeIndexingProgress.visibility = View.INVISIBLE binding.homeIndexingProgress.visibility = View.INVISIBLE
when (error) { when (error) {
is NoAudioPermissionException -> { is NoAudioPermissionException -> {
logD("Updating UI to permission request state") logD("Showing permission prompt")
binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms) binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms)
// Configure the action to act as a permission launcher. // Configure the action to act as a permission launcher.
binding.homeIndexingAction.apply { binding.homeIndexingAction.apply {
@ -401,7 +437,7 @@ class HomeFragment :
} }
} }
is NoMusicException -> { is NoMusicException -> {
logD("Updating UI to no music state") logD("Showing no music error")
binding.homeIndexingStatus.text = context.getString(R.string.err_no_music) binding.homeIndexingStatus.text = context.getString(R.string.err_no_music)
// Configure the action to act as a reload trigger. // Configure the action to act as a reload trigger.
binding.homeIndexingAction.apply { binding.homeIndexingAction.apply {
@ -411,7 +447,7 @@ class HomeFragment :
} }
} }
else -> { else -> {
logD("Updating UI to error state") logD("Showing generic error")
binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed) binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed)
// Configure the action to act as a reload trigger. // Configure the action to act as a reload trigger.
binding.homeIndexingAction.apply { binding.homeIndexingAction.apply {
@ -431,11 +467,13 @@ class HomeFragment :
when (progress) { when (progress) {
is IndexingProgress.Indeterminate -> { is IndexingProgress.Indeterminate -> {
logD("Showing generic progress")
// In a query/initialization state, show a generic loading status. // In a query/initialization state, show a generic loading status.
binding.homeIndexingStatus.text = getString(R.string.lng_indexing) binding.homeIndexingStatus.text = getString(R.string.lng_indexing)
binding.homeIndexingProgress.isIndeterminate = true binding.homeIndexingProgress.isIndeterminate = true
} }
is IndexingProgress.Songs -> { is IndexingProgress.Songs -> {
logD("Showing song progress")
// Actively loading songs, show the current progress. // Actively loading songs, show the current progress.
binding.homeIndexingStatus.text = binding.homeIndexingStatus.text =
getString(R.string.fmt_indexing, progress.current, progress.total) 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 // 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. // popup to overlap with the FAB, so we hide the FAB when fast scrolling too.
if (songs.isEmpty() || isFastScrolling) { if (songs.isEmpty() || isFastScrolling) {
logD("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling")
binding.homeFab.hide() binding.homeFab.hide()
} else { } else {
logD("Showing fab")
binding.homeFab.show() binding.homeFab.show()
} }
} }

View file

@ -26,6 +26,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -67,15 +68,18 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context)
override fun migrate() { override fun migrate() {
if (sharedPreferences.contains(OLD_KEY_LIB_TABS)) { if (sharedPreferences.contains(OLD_KEY_LIB_TABS)) {
logD("Migrating tab setting")
val oldTabs = val oldTabs =
Tab.fromIntCode(sharedPreferences.getInt(OLD_KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT)) Tab.fromIntCode(sharedPreferences.getInt(OLD_KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT))
?: unlikelyToBeNull(Tab.fromIntCode(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. // The playlist tab is now parsed, but it needs to be made visible.
val playlistIndex = oldTabs.indexOfFirst { it.mode == MusicMode.PLAYLISTS } val playlistIndex = oldTabs.indexOfFirst { it.mode == MusicMode.PLAYLISTS }
if (playlistIndex > -1) { // Sanity check check(playlistIndex > -1) // This should exist, otherwise we are in big trouble
oldTabs[playlistIndex] = Tab.Visible(MusicMode.PLAYLISTS) oldTabs[playlistIndex] = Tab.Visible(MusicMode.PLAYLISTS)
} logD("New tabs: $oldTabs")
sharedPreferences.edit { sharedPreferences.edit {
putInt(getString(R.string.set_key_home_tabs), Tab.toIntCode(oldTabs)) putInt(getString(R.string.set_key_home_tabs), Tab.toIntCode(oldTabs))
remove(OLD_KEY_LIB_TABS) remove(OLD_KEY_LIB_TABS)
@ -85,8 +89,14 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context)
override fun onSettingChanged(key: String, listener: HomeSettings.Listener) { override fun onSettingChanged(key: String, listener: HomeSettings.Listener) {
when (key) { when (key) {
getString(R.string.set_key_home_tabs) -> listener.onTabsChanged() getString(R.string.set_key_home_tabs) -> {
getString(R.string.set_key_hide_collaborators) -> listener.onHideCollaboratorsChanged() logD("Dispatching tab setting change")
listener.onTabsChanged()
}
getString(R.string.set_key_hide_collaborators) -> {
logD("Dispatching collaborator setting change")
listener.onHideCollaboratorsChanged()
}
} }
} }

View file

@ -75,8 +75,7 @@ constructor(
private val _artistsList = MutableStateFlow(listOf<Artist>()) private val _artistsList = MutableStateFlow(listOf<Artist>())
/** /**
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that * 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 * if "Hide collaborators" is on, this list will not include collaborator [Artist]s.
* [Artist.isCollaborator] is true.
*/ */
val artistsList: MutableStateFlow<List<Artist>> val artistsList: MutableStateFlow<List<Artist>>
get() = _artistsList get() = _artistsList
@ -157,9 +156,11 @@ constructor(
_artistsList.value = _artistsList.value =
musicSettings.artistSort.artists( musicSettings.artistSort.artists(
if (homeSettings.shouldHideCollaborators) { if (homeSettings.shouldHideCollaborators) {
logD("Filtering collaborator artists")
// Hide Collaborators is enabled, filter out collaborators. // Hide Collaborators is enabled, filter out collaborators.
deviceLibrary.artists.filter { it.explicitAlbums.isNotEmpty() } deviceLibrary.artists.filter { it.explicitAlbums.isNotEmpty() }
} else { } else {
logD("Using all artists")
deviceLibrary.artists deviceLibrary.artists
}) })
_genresInstructions.put(UpdateInstructions.Diff) _genresInstructions.put(UpdateInstructions.Diff)
@ -177,12 +178,14 @@ constructor(
override fun onTabsChanged() { override fun onTabsChanged() {
// Tabs changed, update the current tabs and set up a re-create event. // Tabs changed, update the current tabs and set up a re-create event.
currentTabModes = makeTabModes() currentTabModes = makeTabModes()
logD("Updating tabs: ${currentTabMode.value}")
_shouldRecreate.put(Unit) _shouldRecreate.put(Unit)
} }
override fun onHideCollaboratorsChanged() { override fun onHideCollaboratorsChanged() {
// Changes in the hide collaborator setting will change the artist contents // Changes in the hide collaborator setting will change the artist contents
// of the library, consider it a library update. // of the library, consider it a library update.
logD("Collaborator setting changed, forwarding update")
onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) 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]. * @param sort The new [Sort] to apply. Assumed to be an allowed sort for the current [Tab].
*/ */
fun setSortForCurrentTab(sort: Sort) { 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. // 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 -> { MusicMode.SONGS -> {
logD("Updating song [$mode] sort mode to $sort")
musicSettings.songSort = sort musicSettings.songSort = sort
_songsInstructions.put(UpdateInstructions.Replace(0)) _songsInstructions.put(UpdateInstructions.Replace(0))
_songsList.value = sort.songs(_songsList.value) _songsList.value = sort.songs(_songsList.value)
} }
MusicMode.ALBUMS -> { MusicMode.ALBUMS -> {
logD("Updating album [$mode] sort mode to $sort")
musicSettings.albumSort = sort musicSettings.albumSort = sort
_albumsInstructions.put(UpdateInstructions.Replace(0)) _albumsInstructions.put(UpdateInstructions.Replace(0))
_albumsLists.value = sort.albums(_albumsLists.value) _albumsLists.value = sort.albums(_albumsLists.value)
} }
MusicMode.ARTISTS -> { MusicMode.ARTISTS -> {
logD("Updating artist [$mode] sort mode to $sort")
musicSettings.artistSort = sort musicSettings.artistSort = sort
_artistsInstructions.put(UpdateInstructions.Replace(0)) _artistsInstructions.put(UpdateInstructions.Replace(0))
_artistsList.value = sort.artists(_artistsList.value) _artistsList.value = sort.artists(_artistsList.value)
} }
MusicMode.GENRES -> { MusicMode.GENRES -> {
logD("Updating genre [$mode] sort mode to $sort")
musicSettings.genreSort = sort musicSettings.genreSort = sort
_genresInstructions.put(UpdateInstructions.Replace(0)) _genresInstructions.put(UpdateInstructions.Replace(0))
_genresList.value = sort.genres(_genresList.value) _genresList.value = sort.genres(_genresList.value)
} }
MusicMode.PLAYLISTS -> { MusicMode.PLAYLISTS -> {
logD("Updating playlist [$mode] sort mode to $sort")
musicSettings.playlistSort = sort musicSettings.playlistSort = sort
_playlistsInstructions.put(UpdateInstructions.Replace(0)) _playlistsInstructions.put(UpdateInstructions.Replace(0))
_playlistsList.value = sort.playlists(_playlistsList.value) _playlistsList.value = sort.playlists(_playlistsList.value)

View file

@ -107,7 +107,7 @@ class AlbumListFragment :
is Sort.Mode.ByArtist -> album.artists[0].name.thumb is Sort.Mode.ByArtist -> album.artists[0].name.thumb
// Date -> Use minimum date (Maximum dates are not sorted by, so showing them is odd) // 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 // Duration -> Use formatted duration
is Sort.Mode.ByDuration -> album.durationMs.formatDurationMs(false) is Sort.Mode.ByDuration -> album.durationMs.formatDurationMs(false)
@ -156,8 +156,8 @@ class AlbumListFragment :
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
// Only highlight the album if it is currently playing, and if the currently // Only highlight the album if it is currently playing, and if the currently
// playing song is also contained within. // playing song is also contained within.
val playlist = (parent as? Album)?.takeIf { song?.album == it } val album = (parent as? Album)?.takeIf { song?.album == it }
albumAdapter.setPlaying(playlist, isPlaying) albumAdapter.setPlaying(album, isPlaying)
} }
/** /**

View file

@ -44,7 +44,6 @@ import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
/** /**
@ -123,7 +122,7 @@ class ArtistListFragment :
} }
private fun updateArtists(artists: List<Artist>) { private fun updateArtists(artists: List<Artist>) {
artistAdapter.update(artists, homeModel.artistsInstructions.consume().also { logD(it) }) artistAdapter.update(artists, homeModel.artistsInstructions.consume())
} }
private fun updateSelection(selection: List<Music>) { private fun updateSelection(selection: List<Music>) {
@ -133,8 +132,8 @@ class ArtistListFragment :
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
// Only highlight the artist if it is currently playing, and if the currently // Only highlight the artist if it is currently playing, and if the currently
// playing song is also contained within. // playing song is also contained within.
val playlist = (parent as? Artist)?.takeIf { song?.run { artists.contains(it) } ?: false } val artist = (parent as? Artist)?.takeIf { song?.run { artists.contains(it) } ?: false }
artistAdapter.setPlaying(playlist, isPlaying) artistAdapter.setPlaying(artist, isPlaying)
} }
/** /**

View file

@ -44,7 +44,6 @@ import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
/** /**
* A [ListFragment] that shows a list of [Genre]s. * A [ListFragment] that shows a list of [Genre]s.
@ -122,7 +121,7 @@ class GenreListFragment :
} }
private fun updateGenres(genres: List<Genre>) { private fun updateGenres(genres: List<Genre>) {
genreAdapter.update(genres, homeModel.genresInstructions.consume().also { logD(it) }) genreAdapter.update(genres, homeModel.genresInstructions.consume())
} }
private fun updateSelection(selection: List<Music>) { private fun updateSelection(selection: List<Music>) {
@ -132,8 +131,8 @@ class GenreListFragment :
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
// Only highlight the genre if it is currently playing, and if the currently // Only highlight the genre if it is currently playing, and if the currently
// playing song is also contained within. // playing song is also contained within.
val playlist = (parent as? Genre)?.takeIf { song?.run { genres.contains(it) } ?: false } val genre = (parent as? Genre)?.takeIf { song?.run { genres.contains(it) } ?: false }
genreAdapter.setPlaying(playlist, isPlaying) genreAdapter.setPlaying(genre, isPlaying)
} }
/** /**

View file

@ -43,7 +43,6 @@ import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
/** /**
* A [ListFragment] that shows a list of [Playlist]s. * A [ListFragment] that shows a list of [Playlist]s.
@ -120,8 +119,7 @@ class PlaylistListFragment :
} }
private fun updatePlaylists(playlists: List<Playlist>) { private fun updatePlaylists(playlists: List<Playlist>) {
playlistAdapter.update( playlistAdapter.update(playlists, homeModel.playlistsInstructions.consume())
playlists, homeModel.playlistsInstructions.consume().also { logD(it) })
} }
private fun updateSelection(selection: List<Music>) { private fun updateSelection(selection: List<Music>) {

View file

@ -23,7 +23,6 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.logD
/** /**
* A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations * A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations
@ -67,20 +66,11 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List<MusicMode>) :
// Use expected sw* size thresholds when choosing a configuration. // Use expected sw* size thresholds when choosing a configuration.
when { when {
// On small screens, only display an icon. // On small screens, only display an icon.
width < 370 -> { width < 370 -> tab.setIcon(icon).setContentDescription(string)
logD("Using icon-only configuration")
tab.setIcon(icon).setContentDescription(string)
}
// On large screens, display an icon and text. // On large screens, display an icon and text.
width < 600 -> { width < 600 -> tab.setText(string)
logD("Using text-only configuration")
tab.setText(string)
}
// On medium-size screens, display text. // On medium-size screens, display text.
else -> { else -> tab.setIcon(icon).setText(string)
logD("Using icon-and-text configuration")
tab.setIcon(icon).setText(string)
}
} }
} }
} }

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.home.tabs
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW
/** /**
* A representation of a library tab suitable for configuration. * A representation of a library tab suitable for configuration.
@ -84,6 +85,10 @@ sealed class Tab(open val mode: MusicMode) {
fun toIntCode(tabs: Array<Tab>): Int { fun toIntCode(tabs: Array<Tab>): Int {
// Like when deserializing, make sure there are no duplicate tabs for whatever reason. // Like when deserializing, make sure there are no duplicate tabs for whatever reason.
val distinct = tabs.distinctBy { it.mode } 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 sequence = 0
var shift = MAX_SEQUENCE_IDX * 4 var shift = MAX_SEQUENCE_IDX * 4
@ -127,6 +132,10 @@ sealed class Tab(open val mode: MusicMode) {
// Make sure there are no duplicate tabs // Make sure there are no duplicate tabs
val distinct = tabs.distinctBy { it.mode } 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. // For safety, return null if we have an empty or larger-than-expected tab array.
if (distinct.isEmpty() || distinct.size < MAX_SEQUENCE_IDX) { if (distinct.isEmpty() || distinct.size < MAX_SEQUENCE_IDX) {

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.list.EditClickListListener
import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.inflater 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. * A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration.
@ -52,6 +53,7 @@ class TabAdapter(private val listener: EditClickListListener<Tab>) :
* @param newTabs The new array of tabs to show. * @param newTabs The new array of tabs to show.
*/ */
fun submitTabs(newTabs: Array<Tab>) { fun submitTabs(newTabs: Array<Tab>) {
logD("Force-updating tab information")
tabs = newTabs tabs = newTabs
@Suppress("NotifyDatasetChanged") notifyDataSetChanged() @Suppress("NotifyDatasetChanged") notifyDataSetChanged()
} }
@ -63,6 +65,7 @@ class TabAdapter(private val listener: EditClickListListener<Tab>) :
* @param tab The new tab. * @param tab The new tab.
*/ */
fun setTab(at: Int, tab: Tab) { fun setTab(at: Int, tab: Tab) {
logD("Updating tab [at: $at, tab: $tab]")
tabs[at] = tab tabs[at] = tab
// Use a payload to avoid an item change animation. // Use a payload to avoid an item change animation.
notifyItemChanged(at, PAYLOAD_TAB_CHANGED) notifyItemChanged(at, PAYLOAD_TAB_CHANGED)
@ -75,6 +78,7 @@ class TabAdapter(private val listener: EditClickListListener<Tab>) :
* @param b The position of the second tab to swap. * @param b The position of the second tab to swap.
*/ */
fun swapTabs(a: Int, b: Int) { fun swapTabs(a: Int, b: Int) {
logD("Swapping tabs [a: $a, b: $b]")
val tmp = tabs[b] val tmp = tabs[b]
tabs[b] = tabs[a] tabs[b] = tabs[a]
tabs[a] = tmp tabs[a] = tmp

View file

@ -91,14 +91,15 @@ class TabCustomizeDialog :
// We will need the exact index of the tab to update on in order to // We will need the exact index of the tab to update on in order to
// notify the adapter of the change. // notify the adapter of the change.
val index = tabAdapter.tabs.indexOfFirst { it.mode == item.mode } val index = tabAdapter.tabs.indexOfFirst { it.mode == item.mode }
val tab = tabAdapter.tabs[index] val old = tabAdapter.tabs[index]
tabAdapter.setTab( val new =
index, when (old) {
when (tab) {
// Invert the visibility of the tab // Invert the visibility of the tab
is Tab.Visible -> Tab.Invisible(tab.mode) is Tab.Visible -> Tab.Invisible(old.mode)
is Tab.Invisible -> Tab.Visible(tab.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. // 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 = (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =

View file

@ -63,7 +63,9 @@ class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callbac
return true 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. // We use a custom drag handle, so disable the long press action.
override fun isLongPressDragEnabled() = false override fun isLongPressDragEnabled() = false

View file

@ -73,6 +73,7 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
override fun onSettingChanged(key: String, listener: ImageSettings.Listener) { override fun onSettingChanged(key: String, listener: ImageSettings.Listener) {
if (key == getString(R.string.set_key_cover_mode)) { if (key == getString(R.string.set_key_cover_mode)) {
logD("Dispatching cover mode setting change")
listener.onCoverModeChanged() listener.onCoverModeChanged()
} }
} }

View file

@ -53,8 +53,7 @@ import org.oxycblt.auxio.image.ImageSettings
import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW
class CoverExtractor class CoverExtractor
@Inject @Inject
@ -97,7 +96,7 @@ constructor(
CoverMode.QUALITY -> extractQualityCover(album) CoverMode.QUALITY -> extractQualityCover(album)
} }
} catch (e: Exception) { } 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 null
} }
@ -154,7 +153,6 @@ constructor(
} }
if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) { if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) {
logD("Front cover found")
stream = ByteArrayInputStream(pic) stream = ByteArrayInputStream(pic)
break break
} else if (stream == null) { } else if (stream == null) {

View file

@ -31,8 +31,7 @@ import kotlin.math.min
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SquareFrameTransform : Transformation { class SquareFrameTransform : Transformation {
override val cacheKey: String override val cacheKey = "SquareFrameTransform"
get() = "SquareFrameTransform"
override suspend fun transform(input: Bitmap, size: Size): Bitmap { override suspend fun transform(input: Bitmap, size: Size): Bitmap {
// Find the smaller dimension and then take a center portion of the image that // Find the smaller dimension and then take a center portion of the image that

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.list
import androidx.annotation.StringRes 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. */ /** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
interface Item interface Item

View file

@ -36,6 +36,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.MainNavigationAction import org.oxycblt.auxio.navigation.MainNavigationAction
import org.oxycblt.auxio.navigation.NavigationViewModel import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.share
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -94,33 +95,40 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
R.id.action_play_next -> { R.id.action_play_next -> {
playbackModel.playNext(song) playbackModel.playNext(song)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_queue_add -> { R.id.action_queue_add -> {
playbackModel.addToQueue(song) playbackModel.addToQueue(song)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_go_artist -> { R.id.action_go_artist -> {
navModel.exploreNavigateToParentArtist(song) navModel.exploreNavigateToParentArtist(song)
true
} }
R.id.action_go_album -> { R.id.action_go_album -> {
navModel.exploreNavigateTo(song.album) navModel.exploreNavigateTo(song.album)
true
} }
R.id.action_share -> { R.id.action_share -> {
requireContext().share(song) requireContext().share(song)
true
} }
R.id.action_playlist_add -> { R.id.action_playlist_add -> {
musicModel.addToPlaylist(song) musicModel.addToPlaylist(song)
true
} }
R.id.action_song_detail -> { R.id.action_song_detail -> {
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionShowDetails(song.uid))) MainFragmentDirections.actionShowDetails(song.uid)))
true
} }
else -> { else -> {
error("Unexpected menu item selected") logW("Unexpected menu item selected")
false
} }
} }
true
} }
} }
} }
@ -141,32 +149,39 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
when (it.itemId) { when (it.itemId) {
R.id.action_play -> { R.id.action_play -> {
playbackModel.play(album) playbackModel.play(album)
true
} }
R.id.action_shuffle -> { R.id.action_shuffle -> {
playbackModel.shuffle(album) playbackModel.shuffle(album)
true
} }
R.id.action_play_next -> { R.id.action_play_next -> {
playbackModel.playNext(album) playbackModel.playNext(album)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_queue_add -> { R.id.action_queue_add -> {
playbackModel.addToQueue(album) playbackModel.addToQueue(album)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_go_artist -> { R.id.action_go_artist -> {
navModel.exploreNavigateToParentArtist(album) navModel.exploreNavigateToParentArtist(album)
true
} }
R.id.action_playlist_add -> { R.id.action_playlist_add -> {
musicModel.addToPlaylist(album) musicModel.addToPlaylist(album)
true
} }
R.id.action_share -> { R.id.action_share -> {
requireContext().share(album) requireContext().share(album)
true
} }
else -> { else -> {
error("Unexpected menu item selected") logW("Unexpected menu item selected")
false
} }
} }
true
} }
} }
} }
@ -184,6 +199,9 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
openMenu(anchor, menuRes) { openMenu(anchor, menuRes) {
val playable = artist.songs.isNotEmpty() 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_play).isEnabled = playable
menu.findItem(R.id.action_shuffle).isEnabled = playable menu.findItem(R.id.action_shuffle).isEnabled = playable
menu.findItem(R.id.action_play_next).isEnabled = playable menu.findItem(R.id.action_play_next).isEnabled = playable
@ -195,29 +213,35 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
when (it.itemId) { when (it.itemId) {
R.id.action_play -> { R.id.action_play -> {
playbackModel.play(artist) playbackModel.play(artist)
true
} }
R.id.action_shuffle -> { R.id.action_shuffle -> {
playbackModel.shuffle(artist) playbackModel.shuffle(artist)
true
} }
R.id.action_play_next -> { R.id.action_play_next -> {
playbackModel.playNext(artist) playbackModel.playNext(artist)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_queue_add -> { R.id.action_queue_add -> {
playbackModel.addToQueue(artist) playbackModel.addToQueue(artist)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_playlist_add -> { R.id.action_playlist_add -> {
musicModel.addToPlaylist(artist) musicModel.addToPlaylist(artist)
true
} }
R.id.action_share -> { R.id.action_share -> {
requireContext().share(artist) requireContext().share(artist)
true
} }
else -> { else -> {
error("Unexpected menu item selected") logW("Unexpected menu item selected")
false
} }
} }
true
} }
} }
} }
@ -238,29 +262,35 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
when (it.itemId) { when (it.itemId) {
R.id.action_play -> { R.id.action_play -> {
playbackModel.play(genre) playbackModel.play(genre)
true
} }
R.id.action_shuffle -> { R.id.action_shuffle -> {
playbackModel.shuffle(genre) playbackModel.shuffle(genre)
true
} }
R.id.action_play_next -> { R.id.action_play_next -> {
playbackModel.playNext(genre) playbackModel.playNext(genre)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_queue_add -> { R.id.action_queue_add -> {
playbackModel.addToQueue(genre) playbackModel.addToQueue(genre)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_playlist_add -> { R.id.action_playlist_add -> {
musicModel.addToPlaylist(genre) musicModel.addToPlaylist(genre)
true
} }
R.id.action_share -> { R.id.action_share -> {
requireContext().share(genre) requireContext().share(genre)
true
} }
else -> { else -> {
error("Unexpected menu item selected") logW("Unexpected menu item selected")
false
} }
} }
true
} }
} }
} }
@ -288,32 +318,39 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
when (it.itemId) { when (it.itemId) {
R.id.action_play -> { R.id.action_play -> {
playbackModel.play(playlist) playbackModel.play(playlist)
true
} }
R.id.action_shuffle -> { R.id.action_shuffle -> {
playbackModel.shuffle(playlist) playbackModel.shuffle(playlist)
true
} }
R.id.action_play_next -> { R.id.action_play_next -> {
playbackModel.playNext(playlist) playbackModel.playNext(playlist)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_queue_add -> { R.id.action_queue_add -> {
playbackModel.addToQueue(playlist) playbackModel.addToQueue(playlist)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_rename -> { R.id.action_rename -> {
musicModel.renamePlaylist(playlist) musicModel.renamePlaylist(playlist)
true
} }
R.id.action_delete -> { R.id.action_delete -> {
musicModel.deletePlaylist(playlist) musicModel.deletePlaylist(playlist)
true
} }
R.id.action_share -> { R.id.action_share -> {
requireContext().share(playlist) requireContext().share(playlist)
true
} }
else -> { else -> {
error("Unexpected menu item selected") logW("Unexpected menu item selected")
false
} }
} }
true
} }
} }
} }
@ -332,6 +369,8 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
return return
} }
logD("Opening popup menu menu")
currentMenu = currentMenu =
PopupMenu(requireContext(), anchor).apply { PopupMenu(requireContext(), anchor).apply {
inflate(menuRes) inflate(menuRes)

View file

@ -22,8 +22,6 @@ import androidx.annotation.IdRes
import kotlin.math.max import kotlin.math.max
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R 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.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre

View file

@ -25,6 +25,7 @@ import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.util.concurrent.Executor import java.util.concurrent.Executor
import org.oxycblt.auxio.util.logD
/** /**
* A variant of ListDiffer with more flexible updates. * A variant of ListDiffer with more flexible updates.
@ -46,15 +47,18 @@ abstract class FlexibleListAdapter<T, VH : RecyclerView.ViewHolder>(
/** /**
* Update the adapter with new data. * 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 instructions The [UpdateInstructions] to visually update the list with.
* @param callback Called when the update is completed. May be done asynchronously. * @param callback Called when the update is completed. May be done asynchronously.
*/ */
fun update( fun update(
newData: List<T>, newList: List<T>,
instructions: UpdateInstructions?, instructions: UpdateInstructions?,
callback: (() -> Unit)? = null 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<T>(
) { ) {
// fast simple remove all // fast simple remove all
if (newList.isEmpty()) { if (newList.isEmpty()) {
logD("Short-circuiting diff to remove all")
val countRemoved = oldList.size val countRemoved = oldList.size
currentList = emptyList() currentList = emptyList()
// notify last, after list is updated // notify last, after list is updated
@ -175,6 +180,7 @@ private class FlexibleListDiffer<T>(
// fast simple first insert // fast simple first insert
if (oldList.isEmpty()) { if (oldList.isEmpty()) {
logD("Short-circuiting diff to insert all")
currentList = newList currentList = newList
// notify last, after list is updated // notify last, after list is updated
updateCallback.onInserted(0, newList.size) updateCallback.onInserted(0, newList.size)
@ -233,8 +239,10 @@ private class FlexibleListDiffer<T>(
throw AssertionError() throw AssertionError()
} }
}) })
mainThreadExecutor.execute { mainThreadExecutor.execute {
if (maxScheduledGeneration == runGeneration) { if (maxScheduledGeneration == runGeneration) {
logD("Applying calculated diff")
currentList = newList currentList = newList
result.dispatchUpdatesTo(updateCallback) result.dispatchUpdatesTo(updateCallback)
callback?.invoke() callback?.invoke()

View file

@ -58,6 +58,8 @@ abstract class PlayingIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
* @param isPlaying Whether playback is ongoing or paused. * @param isPlaying Whether playback is ongoing or paused.
*/ */
fun setPlaying(item: T?, isPlaying: Boolean) { fun setPlaying(item: T?, isPlaying: Boolean) {
logD("Updating playing item [old: $currentItem new: $item]")
var updatedItem = false var updatedItem = false
if (currentItem != item) { if (currentItem != item) {
val oldItem = currentItem val oldItem = currentItem

View file

@ -22,6 +22,7 @@ import android.view.View
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.Music 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 * A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of
@ -54,6 +55,7 @@ abstract class SelectionIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
// Nothing to do. // Nothing to do.
return return
} }
logD("Updating selection [old=${oldSelectedItems.size} new=${newSelectedItems.size}")
selectedItems = newSelectedItems selectedItems = newSelectedItems
for (i in currentList.indices) { for (i in currentList.indices) {

View file

@ -68,7 +68,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() {
// this is only done once when the item is initially picked up. // this is only done once when the item is initially picked up.
// TODO: I think this is possible to improve with a raw ValueAnimator. // TODO: I think this is possible to improve with a raw ValueAnimator.
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) { if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
logD("Lifting item") logD("Lifting ViewHolder")
val bg = holder.background val bg = holder.background
val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal) 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 // This function can be called multiple times, so only start the animation when the view's
// translationZ is already non-zero. // translationZ is already non-zero.
if (holder.root.translationZ != 0f) { if (holder.root.translationZ != 0f) {
logD("Dropping item") logD("Lifting ViewHolder")
val bg = holder.background val bg = holder.background
val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal) 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. // Long-press events are too buggy, only allow dragging with the handle.
final override fun isLongPressDragEnabled() = false final override fun isLongPressDragEnabled() = false
/** Required [RecyclerView.ViewHolder] implementation that exposes the following. */ /** Required [RecyclerView.ViewHolder] implementation that exposes required fields */
interface ViewHolder { interface ViewHolder {
/** Whether this [ViewHolder] can be moved right now. */ /** Whether this [ViewHolder] can be moved right now. */
val enabled: Boolean val enabled: Boolean

View file

@ -27,10 +27,12 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
/** /**
* A [ViewModel] that manages the current selection. * A [ViewModel] that manages the current selection.
@ -83,10 +85,19 @@ constructor(
* @param music The [Music] item to select. * @param music The [Music] item to select.
*/ */
fun select(music: Music) { fun select(music: Music) {
if (music is MusicParent && music.songs.isEmpty()) {
logD("Cannot select empty parent, ignoring operation")
return
}
val selected = _selected.value.toMutableList() val selected = _selected.value.toMutableList()
if (!selected.remove(music)) { if (!selected.remove(music)) {
logD("Adding $music to selection")
selected.add(music) selected.add(music)
} else {
logD("Removed $music from selection")
} }
_selected.value = selected _selected.value = selected
} }
@ -95,8 +106,9 @@ constructor(
* *
* @return A list of [Song]s collated from each item selected. * @return A list of [Song]s collated from each item selected.
*/ */
fun take() = fun take(): List<Song> {
_selected.value logD("Taking selection")
return _selected.value
.flatMap { .flatMap {
when (it) { when (it) {
is Song -> listOf(it) is Song -> listOf(it)
@ -106,12 +118,16 @@ constructor(
is Playlist -> it.songs is Playlist -> it.songs
} }
} }
.also { drop() } .also { _selected.value = listOf() }
}
/** /**
* Clear the current selection. * Clear the current selection.
* *
* @return true if the prior selection was non-empty, false otherwise. * @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() }
}
} }

View file

@ -34,9 +34,6 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield 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.cache.CacheRepository
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.device.RawSong 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]. * music (loading) can be reacted to with [UpdateListener] and [IndexingListener].
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*
* TODO: Switch listener to set
*/ */
interface MusicRepository { interface MusicRepository {
/** The current music information found on the device. */ /** The current music information found on the device. */
@ -289,36 +288,42 @@ constructor(
override suspend fun createPlaylist(name: String, songs: List<Song>) { override suspend fun createPlaylist(name: String, songs: List<Song>) {
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Creating playlist $name with ${songs.size} songs")
userLibrary.createPlaylist(name, songs) userLibrary.createPlaylist(name, songs)
notifyUserLibraryChange() notifyUserLibraryChange()
} }
override suspend fun renamePlaylist(playlist: Playlist, name: String) { override suspend fun renamePlaylist(playlist: Playlist, name: String) {
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Renaming $playlist to $name")
userLibrary.renamePlaylist(playlist, name) userLibrary.renamePlaylist(playlist, name)
notifyUserLibraryChange() notifyUserLibraryChange()
} }
override suspend fun deletePlaylist(playlist: Playlist) { override suspend fun deletePlaylist(playlist: Playlist) {
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Deleting $playlist")
userLibrary.deletePlaylist(playlist) userLibrary.deletePlaylist(playlist)
notifyUserLibraryChange() notifyUserLibraryChange()
} }
override suspend fun addToPlaylist(songs: List<Song>, playlist: Playlist) { override suspend fun addToPlaylist(songs: List<Song>, playlist: Playlist) {
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Adding ${songs.size} songs to $playlist")
userLibrary.addToPlaylist(playlist, songs) userLibrary.addToPlaylist(playlist, songs)
notifyUserLibraryChange() notifyUserLibraryChange()
} }
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) { override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) {
val userLibrary = synchronized(this) { userLibrary ?: return } val userLibrary = synchronized(this) { userLibrary ?: return }
logD("Rewriting $playlist with ${songs.size} songs")
userLibrary.rewritePlaylist(playlist, songs) userLibrary.rewritePlaylist(playlist, songs)
notifyUserLibraryChange() notifyUserLibraryChange()
} }
@Synchronized @Synchronized
private fun notifyUserLibraryChange() { private fun notifyUserLibraryChange() {
logD("Dispatching user library change")
for (listener in updateListeners) { for (listener in updateListeners) {
listener.onMusicChanges( listener.onMusicChanges(
MusicRepository.Changes(deviceLibrary = false, userLibrary = true)) MusicRepository.Changes(deviceLibrary = false, userLibrary = true))
@ -327,6 +332,7 @@ constructor(
@Synchronized @Synchronized
override fun requestIndex(withCache: Boolean) { override fun requestIndex(withCache: Boolean) {
logD("Requesting index operation [cache=$withCache]")
indexingWorker?.requestIndex(withCache) indexingWorker?.requestIndex(withCache)
} }
@ -353,7 +359,7 @@ constructor(
private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) { private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
if (ContextCompat.checkSelfPermission(worker.context, PERMISSION_READ_AUDIO) == if (ContextCompat.checkSelfPermission(worker.context, PERMISSION_READ_AUDIO) ==
PackageManager.PERMISSION_DENIED) { PackageManager.PERMISSION_DENIED) {
logE("Permission check failed") logE("Permissions were not granted")
// No permissions, signal that we can't do anything. // No permissions, signal that we can't do anything.
throw NoAudioPermissionException() throw NoAudioPermissionException()
} }
@ -363,14 +369,16 @@ constructor(
emitLoading(IndexingProgress.Indeterminate) emitLoading(IndexingProgress.Indeterminate)
// Do the initial query of the cache and media databases in parallel. // 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 mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() }
val cache = val cache =
if (withCache) { if (withCache) {
logD("Reading cache")
cacheRepository.readCache() cacheRepository.readCache()
} else { } else {
null null
} }
logD("Awaiting MediaStore query")
val query = mediaStoreQueryJob.await().getOrThrow() val query = mediaStoreQueryJob.await().getOrThrow()
// Now start processing the queried song information in parallel. Songs that can't be // Now start processing the queried song information in parallel. Songs that can't be
@ -379,11 +387,13 @@ constructor(
logD("Starting song discovery") logD("Starting song discovery")
val completeSongs = Channel<RawSong>(Channel.UNLIMITED) val completeSongs = Channel<RawSong>(Channel.UNLIMITED)
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED) val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED)
logD("Started MediaStore discovery")
val mediaStoreJob = val mediaStoreJob =
worker.scope.tryAsync { worker.scope.tryAsync {
mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs) mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs)
incompleteSongs.close() incompleteSongs.close()
} }
logD("Started ExoPlayer discovery")
val metadataJob = val metadataJob =
worker.scope.tryAsync { worker.scope.tryAsync {
tagExtractor.consume(incompleteSongs, completeSongs) tagExtractor.consume(incompleteSongs, completeSongs)
@ -396,7 +406,8 @@ constructor(
rawSongs.add(rawSong) rawSongs.add(rawSong)
emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) 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() mediaStoreJob.await().getOrThrow()
metadataJob.await().getOrThrow() metadataJob.await().getOrThrow()
@ -411,25 +422,35 @@ constructor(
// TODO: Indicate playlist state in loading process? // TODO: Indicate playlist state in loading process?
emitLoading(IndexingProgress.Indeterminate) emitLoading(IndexingProgress.Indeterminate)
val deviceLibraryChannel = Channel<DeviceLibrary>() val deviceLibraryChannel = Channel<DeviceLibrary>()
logD("Starting DeviceLibrary creation")
val deviceLibraryJob = val deviceLibraryJob =
worker.scope.tryAsync(Dispatchers.Main) { worker.scope.tryAsync(Dispatchers.Main) {
deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) } deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) }
} }
logD("Starting UserLibrary creation")
val userLibraryJob = val userLibraryJob =
worker.scope.tryAsync { worker.scope.tryAsync {
userLibraryFactory.read(deviceLibraryChannel).also { deviceLibraryChannel.close() } userLibraryFactory.read(deviceLibraryChannel).also { deviceLibraryChannel.close() }
} }
if (cache == null || cache.invalidated) { if (cache == null || cache.invalidated) {
logD("Writing cache [why=${cache?.invalidated}]")
cacheRepository.writeCache(rawSongs) cacheRepository.writeCache(rawSongs)
} }
logD("Awaiting library creation")
val deviceLibrary = deviceLibraryJob.await().getOrThrow() val deviceLibrary = deviceLibraryJob.await().getOrThrow()
val userLibrary = userLibraryJob.await().getOrThrow() val userLibrary = userLibraryJob.await().getOrThrow()
logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
emitComplete(null) emitComplete(null)
emitData(deviceLibrary, userLibrary) 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 <R> CoroutineScope.tryAsync( private inline fun <R> CoroutineScope.tryAsync(
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
crossinline block: suspend () -> R crossinline block: suspend () -> R
@ -457,6 +478,7 @@ constructor(
synchronized(this) { synchronized(this) {
previousCompletedState = IndexingState.Completed(error) previousCompletedState = IndexingState.Completed(error)
currentIndexingState = null currentIndexingState = null
logD("Dispatching completion state [error=$error]")
for (listener in indexingListeners) { for (listener in indexingListeners) {
listener.onIndexingStateChanged() listener.onIndexingStateChanged()
} }
@ -472,6 +494,7 @@ constructor(
this.deviceLibrary = deviceLibrary this.deviceLibrary = deviceLibrary
this.userLibrary = userLibrary this.userLibrary = userLibrary
val changes = MusicRepository.Changes(deviceLibraryChanged, userLibraryChanged) val changes = MusicRepository.Changes(deviceLibraryChanged, userLibraryChanged)
logD("Dispatching library change [changes=$changes]")
for (listener in updateListeners) { for (listener in updateListeners) {
listener.onMusicChanges(changes) listener.onMusicChanges(changes)
} }

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.fs.Directory
import org.oxycblt.auxio.music.fs.MusicDirectories import org.oxycblt.auxio.music.fs.MusicDirectories
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD
/** /**
* User configuration specific to music system. * 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),
getString(R.string.set_key_music_dirs_include), getString(R.string.set_key_music_dirs_include),
getString(R.string.set_key_separators), getString(R.string.set_key_separators),
getString(R.string.set_key_auto_sort_names) -> listener.onIndexingSettingChanged() getString(R.string.set_key_auto_sort_names) -> {
getString(R.string.set_key_observing) -> listener.onObservingChanged() logD("Dispatching indexing setting change for $key")
listener.onIndexingSettingChanged()
}
getString(R.string.set_key_observing) -> {
logD("Dispatching observing setting change")
listener.onObservingChanged()
}
} }
} }
} }

View file

@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.MutableEvent
import org.oxycblt.auxio.util.logD
/** /**
* A [ViewModel] providing data specific to the music loading process. * A [ViewModel] providing data specific to the music loading process.
@ -89,6 +90,7 @@ constructor(
deviceLibrary.artists.size, deviceLibrary.artists.size,
deviceLibrary.genres.size, deviceLibrary.genres.size,
deviceLibrary.songs.sumOf { it.durationMs }) deviceLibrary.songs.sumOf { it.durationMs })
logD("Updated statistics: ${_statistics.value}")
} }
override fun onIndexingStateChanged() { override fun onIndexingStateChanged() {
@ -97,11 +99,13 @@ constructor(
/** Requests that the music library should be re-loaded while leveraging the cache. */ /** Requests that the music library should be re-loaded while leveraging the cache. */
fun refresh() { fun refresh() {
logD("Refreshing library")
musicRepository.requestIndex(true) musicRepository.requestIndex(true)
} }
/** Requests that the music library be re-loaded without the cache. */ /** Requests that the music library be re-loaded without the cache. */
fun rescan() { fun rescan() {
logD("Rescanning library")
musicRepository.requestIndex(false) musicRepository.requestIndex(false)
} }
@ -113,8 +117,10 @@ constructor(
*/ */
fun createPlaylist(name: String? = null, songs: List<Song> = listOf()) { fun createPlaylist(name: String? = null, songs: List<Song> = listOf()) {
if (name != null) { if (name != null) {
logD("Creating $name with ${songs.size} songs]")
viewModelScope.launch(Dispatchers.IO) { musicRepository.createPlaylist(name, songs) } viewModelScope.launch(Dispatchers.IO) { musicRepository.createPlaylist(name, songs) }
} else { } else {
logD("Launching creation dialog for ${songs.size} songs")
_newPlaylistSongs.put(songs) _newPlaylistSongs.put(songs)
} }
} }
@ -127,8 +133,10 @@ constructor(
*/ */
fun renamePlaylist(playlist: Playlist, name: String? = null) { fun renamePlaylist(playlist: Playlist, name: String? = null) {
if (name != null) { if (name != null) {
logD("Renaming $playlist to $name")
viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) } viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) }
} else { } else {
logD("Launching rename dialog for $playlist")
_playlistToRename.put(playlist) _playlistToRename.put(playlist)
} }
} }
@ -142,8 +150,10 @@ constructor(
*/ */
fun deletePlaylist(playlist: Playlist, rude: Boolean = false) { fun deletePlaylist(playlist: Playlist, rude: Boolean = false) {
if (rude) { if (rude) {
logD("Deleting $playlist")
viewModelScope.launch(Dispatchers.IO) { musicRepository.deletePlaylist(playlist) } viewModelScope.launch(Dispatchers.IO) { musicRepository.deletePlaylist(playlist) }
} else { } else {
logD("Launching deletion dialog for $playlist")
_playlistToDelete.put(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. * @param playlist The [Playlist] to add to. If null, the user will be prompted for one.
*/ */
fun addToPlaylist(song: Song, playlist: Playlist? = null) { fun addToPlaylist(song: Song, playlist: Playlist? = null) {
logD("Adding $song to playlist")
addToPlaylist(listOf(song), 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. * @param playlist The [Playlist] to add to. If null, the user will be prompted for one.
*/ */
fun addToPlaylist(album: Album, playlist: Playlist? = null) { fun addToPlaylist(album: Album, playlist: Playlist? = null) {
logD("Adding $album to playlist")
addToPlaylist(musicSettings.albumSongSort.songs(album.songs), 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. * @param playlist The [Playlist] to add to. If null, the user will be prompted for one.
*/ */
fun addToPlaylist(artist: Artist, playlist: Playlist? = null) { fun addToPlaylist(artist: Artist, playlist: Playlist? = null) {
logD("Adding $artist to playlist")
addToPlaylist(musicSettings.artistSongSort.songs(artist.songs), 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. * @param playlist The [Playlist] to add to. If null, the user will be prompted for one.
*/ */
fun addToPlaylist(genre: Genre, playlist: Playlist? = null) { fun addToPlaylist(genre: Genre, playlist: Playlist? = null) {
logD("Adding $genre to playlist")
addToPlaylist(musicSettings.genreSongSort.songs(genre.songs), playlist) addToPlaylist(musicSettings.genreSongSort.songs(genre.songs), playlist)
} }
@ -196,8 +210,10 @@ constructor(
*/ */
fun addToPlaylist(songs: List<Song>, playlist: Playlist? = null) { fun addToPlaylist(songs: List<Song>, playlist: Playlist? = null) {
if (playlist != null) { if (playlist != null) {
logD("Adding ${songs.size} songs to $playlist")
viewModelScope.launch(Dispatchers.IO) { musicRepository.addToPlaylist(songs, playlist) } viewModelScope.launch(Dispatchers.IO) { musicRepository.addToPlaylist(songs, playlist) }
} else { } else {
logD("Launching addition dialog for songs=${songs.size}")
_songsToAdd.put(songs) _songsToAdd.put(songs)
} }
} }

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.cache
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
/** /**
@ -49,7 +50,9 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached
try { try {
// Faster to load the whole database into memory than do a query on each // Faster to load the whole database into memory than do a query on each
// populate call. // populate call.
CacheImpl(cachedSongsDao.readSongs()) val songs = cachedSongsDao.readSongs()
logD("Successfully read ${songs.size} songs from cache")
CacheImpl(songs)
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to load cache database.") logE("Unable to load cache database.")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
@ -60,7 +63,9 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached
try { try {
// Still write out whatever data was extracted. // Still write out whatever data was extracted.
cachedSongsDao.nukeSongs() cachedSongsDao.nukeSongs()
logD("Successfully deleted old cache")
cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw)) cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw))
logD("Successfully wrote ${rawSongs.size} songs to cache")
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to save cache database.") logE("Unable to save cache database.")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
@ -96,7 +101,6 @@ private class CacheImpl(cachedSongs: List<CachedSong>) : Cache {
override var invalidated = false override var invalidated = false
override fun populate(rawSong: RawSong): Boolean { override fun populate(rawSong: RawSong): Boolean {
// For a cached raw song to be used, it must exist within the cache and have matching // 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 // addition and modification timestamps. Technically the addition timestamp doesn't
// exist, but to safeguard against possible OEM-specific timestamp incoherence, we // exist, but to safeguard against possible OEM-specific timestamp incoherence, we

View file

@ -149,6 +149,9 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
return hashCode 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 findSong(uid: Music.UID) = songUidMap[uid]
override fun findAlbum(uid: Music.UID) = albumUidMap[uid] override fun findAlbum(uid: Music.UID) = albumUidMap[uid]
override fun findArtist(uid: Music.UID) = artistUidMap[uid] override fun findArtist(uid: Music.UID) = artistUidMap[uid]

View file

@ -96,6 +96,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
override fun hashCode() = 31 * uid.hashCode() + rawSong.hashCode() override fun hashCode() = 31 * uid.hashCode() + rawSong.hashCode()
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is SongImpl && uid == other.uid && rawSong == other.rawSong 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 artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings)
private val artistNames = rawSong.artistNames.parseMultiValue(musicSettings) private val artistNames = rawSong.artistNames.parseMultiValue(musicSettings)
@ -262,6 +263,8 @@ class AlbumImpl(
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs
override fun toString() = "Album(uid=$uid, name=$name)"
private val _artists = mutableListOf<ArtistImpl>() private val _artists = mutableListOf<ArtistImpl>()
override val artists: List<Artist> override val artists: List<Artist>
get() = _artists get() = _artists
@ -363,6 +366,8 @@ class ArtistImpl(
rawArtist == other.rawArtist && rawArtist == other.rawArtist &&
songs == other.songs songs == other.songs
override fun toString() = "Artist(uid=$uid, name=$name)"
override lateinit var genres: List<Genre> override lateinit var genres: List<Genre>
init { init {
@ -449,6 +454,8 @@ class GenreImpl(
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs
override fun toString() = "Genre(uid=$uid, name=$name)"
init { init {
val distinctAlbums = mutableSetOf<Album>() val distinctAlbums = mutableSetOf<Album>()
val distinctArtists = mutableSetOf<Artist>() val distinctArtists = mutableSetOf<Artist>()

View file

@ -25,6 +25,7 @@ import org.oxycblt.auxio.databinding.ItemMusicDirBinding
import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/** /**
* [RecyclerView.Adapter] that manages a list of [Directory] instances. * [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. * @param dir The [Directory] to add.
*/ */
fun add(dir: Directory) { fun add(dir: Directory) {
if (_dirs.contains(dir)) { if (_dirs.contains(dir)) return
return logD("Adding $dir")
}
_dirs.add(dir) _dirs.add(dir)
notifyItemInserted(_dirs.lastIndex) 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. * 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<Directory>) { fun addAll(dirs: List<Directory>) {
logD("Adding ${dirs.size} directories")
val oldLastIndex = dirs.lastIndex val oldLastIndex = dirs.lastIndex
_dirs.addAll(dirs) _dirs.addAll(dirs)
notifyItemRangeInserted(oldLastIndex, dirs.size) 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. * @param dir The [Directory] to remove. Must exist in the list.
*/ */
fun remove(dir: Directory) { fun remove(dir: Directory) {
logD("Removing $dir")
val idx = _dirs.indexOf(dir) val idx = _dirs.indexOf(dir)
_dirs.removeAt(idx) _dirs.removeAt(idx)
notifyItemRemoved(idx) notifyItemRemoved(idx)
@ -86,6 +87,7 @@ class DirectoryAdapter(private val listener: Listener) :
/** A Listener for [DirectoryAdapter] interactions. */ /** A Listener for [DirectoryAdapter] interactions. */
interface Listener { interface Listener {
/** Called when the delete button on a directory item is clicked. */
fun onRemoveDirectory(dir: Directory) fun onRemoveDirectory(dir: Directory)
} }
} }

View file

@ -145,18 +145,10 @@ data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolea
* @param fromFormat The mime type obtained by analyzing the file format. Null if could not be * @param fromFormat The mime type obtained by analyzing the file format. Null if could not be
* obtained. * obtained.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*
* TODO: Get around to simplifying this
*/ */
data class MimeType(val fromExtension: String, val fromFormat: String?) { 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". * Resolve the mime type into a human-readable format name, such as "Ogg Vorbis".
* *

View file

@ -120,6 +120,7 @@ private abstract class BaseMediaStoreExtractor(
if (dirs.dirs.isNotEmpty()) { if (dirs.dirs.isNotEmpty()) {
selector += " AND " selector += " AND "
if (!dirs.shouldInclude) { if (!dirs.shouldInclude) {
logD("Excluding directories in selector")
// Without a NOT, the query will be restricted to the specified paths, resulting // 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, // in the "Include" mode. With a NOT, the specified paths will not be included,
// resulting in the "Exclude" mode. // resulting in the "Exclude" mode.
@ -144,14 +145,14 @@ private abstract class BaseMediaStoreExtractor(
} }
// Now we can actually query MediaStore. // 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 = val cursor =
context.contentResolverSafe.safeQuery( context.contentResolverSafe.safeQuery(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection, projection,
selector, selector,
args.toTypedArray()) args.toTypedArray())
logD("Song query succeeded [Projected total: ${cursor.count}]") logD("Successfully queried for ${cursor.count} songs")
val genreNamesMap = mutableMapOf<Long, String>() val genreNamesMap = mutableMapOf<Long, String>()
@ -185,6 +186,7 @@ private abstract class BaseMediaStoreExtractor(
} }
} }
} }
logD("Read ${genreNamesMap.size} genres from MediaStore")
logD("Finished initialization in ${System.currentTimeMillis() - start}ms") logD("Finished initialization in ${System.currentTimeMillis() - start}ms")
return wrapQuery(cursor, genreNamesMap) return wrapQuery(cursor, genreNamesMap)

View file

@ -24,6 +24,7 @@ import java.text.SimpleDateFormat
import kotlin.math.max import kotlin.math.max
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.inRangeOrNull import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
/** /**
@ -51,27 +52,25 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
* 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
* be properly localized. * be properly localized.
*/ */
fun resolveDate(context: Context): String { fun resolve(context: Context) =
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)
}
}
// Unable to create fine-grained date, just format as a year. // 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() override fun hashCode() = tokens.hashCode()
@ -139,9 +138,9 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
fun resolveDate(context: Context) = fun resolveDate(context: Context) =
if (min != max) { if (min != max) {
context.getString( 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 { } else {
min.resolveDate(context) min.resolve(context)
} }
override fun equals(other: Any?) = other is Range && min == other.min && max == other.max override fun equals(other: Any?) = other is Range && min == other.min && max == other.max

View file

@ -27,6 +27,7 @@ import org.oxycblt.auxio.list.Item
* @param name The name of the disc group, if any. Null if not present. * @param name The name of the disc group, if any. Null if not present.
*/ */
class Disc(val number: Int, val name: String?) : Item, Comparable<Disc> { class Disc(val number: Int, val name: String?) : Item, Comparable<Disc> {
// 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 equals(other: Any?) = other is Disc && number == other.number
override fun hashCode() = number.hashCode() override fun hashCode() = number.hashCode()
override fun compareTo(other: Disc) = number.compareTo(other.number) override fun compareTo(other: Disc) = number.compareTo(other.number)

View file

@ -201,6 +201,7 @@ private data class IntelligentKnownName(override val raw: String, override val s
// Separate each token into their numeric and lexicographic counterparts. // Separate each token into their numeric and lexicographic counterparts.
if (token.first().isDigit()) { if (token.first().isDigit()) {
// The digit string comparison breaks with preceding zero digits, remove those // 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 } val digits = token.trimStart('0').ifEmpty { token }
// Other languages have other types of digit strings, still use collation keys // Other languages have other types of digit strings, still use collation keys
collationKey = COLLATOR.getCollationKey(digits) collationKey = COLLATOR.getCollationKey(digits)

View file

@ -104,25 +104,23 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties.
null null
} }
val resolvedMimeType = // The song's mime type won't have a populated format field right now, try to
if (song.mimeType.fromFormat != null) { // extract it ourselves.
// ExoPlayer was already able to populate the format. val formatMimeType =
song.mimeType try {
} else { format.getString(MediaFormat.KEY_MIME)
// ExoPlayer couldn't populate the format somehow, populate it here. } catch (e: NullPointerException) {
val formatMimeType = logE("Unable to extract mime type field")
try { null
format.getString(MediaFormat.KEY_MIME)
} catch (e: NullPointerException) {
logE("Unable to extract mime type field")
null
}
MimeType(song.mimeType.fromExtension, formatMimeType)
} }
extractor.release() extractor.release()
return AudioProperties(bitrate, sampleRate, resolvedMimeType) logD("Finished extracting audio properties")
return AudioProperties(
bitrate,
sampleRate,
MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType))
} }
} }

View file

@ -30,12 +30,15 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment 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 * A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to
* split tags with multiple values. * split tags with multiple values.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*
* TODO: Replace with unsplit names dialog
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() { class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
@ -74,7 +77,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
Separators.SLASH -> binding.separatorSlash.isChecked = true Separators.SLASH -> binding.separatorSlash.isChecked = true
Separators.PLUS -> binding.separatorPlus.isChecked = true Separators.PLUS -> binding.separatorPlus.isChecked = true
Separators.AND -> binding.separatorAnd.isChecked = true Separators.AND -> binding.separatorAnd.isChecked = true
else -> error("Unexpected separator in settings data") else -> logW("Unexpected separator in settings data")
} }
} }
} }

View file

@ -23,6 +23,7 @@ import javax.inject.Inject
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.device.RawSong 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 * 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. // producing similar throughput's to other kinds of manual metadata extraction.
val tagWorkerPool: Array<TagWorker?> = arrayOfNulls(TASK_CAPACITY) val tagWorkerPool: Array<TagWorker?> = arrayOfNulls(TASK_CAPACITY)
logD("Beginning primary extraction loop")
for (incompleteRawSong in incompleteSongs) { for (incompleteRawSong in incompleteSongs) {
spin@ while (true) { spin@ while (true) {
for (i in tagWorkerPool.indices) { 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 { do {
var ongoingTasks = false var ongoingTasks = false
for (i in tagWorkerPool.indices) { for (i in tagWorkerPool.indices) {

View file

@ -89,12 +89,8 @@ private class TagWorkerImpl(
} catch (e: Exception) { } catch (e: Exception) {
logW("Unable to extract metadata for ${rawSong.name}") logW("Unable to extract metadata for ${rawSong.name}")
logW(e.stackTraceToString()) logW(e.stackTraceToString())
null return rawSong
} }
if (format == null) {
logD("Nothing could be extracted for ${rawSong.name}")
return rawSong
}
val metadata = format.metadata val metadata = format.metadata
if (metadata != null) { if (metadata != null) {

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
/** /**
@ -93,7 +94,7 @@ class AddToPlaylistDialog :
private fun updatePendingSongs(songs: List<Song>?) { private fun updatePendingSongs(songs: List<Song>?) {
if (songs == null) { if (songs == null) {
// No songs to feasibly add to a playlist, leave. logD("No songs to show choices for, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
} }
} }

View file

@ -32,6 +32,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -76,7 +77,7 @@ class DeletePlaylistDialog : ViewBindingDialogFragment<DialogDeletePlaylistBindi
private fun updatePlaylistToDelete(playlist: Playlist?) { private fun updatePlaylistToDelete(playlist: Playlist?) {
if (playlist == null) { if (playlist == null) {
// Playlist does not exist anymore, leave logD("No playlist to delete, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
return return
} }

View file

@ -32,6 +32,7 @@ import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -89,6 +90,7 @@ class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>()
private fun updatePendingPlaylist(pendingPlaylist: PendingPlaylist?) { private fun updatePendingPlaylist(pendingPlaylist: PendingPlaylist?) {
if (pendingPlaylist == null) { if (pendingPlaylist == null) {
logD("No playlist to create, leaving")
findNavController().navigateUp() findNavController().navigateUp()
return return
} }

View file

@ -31,6 +31,9 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song 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. * 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.preferredName,
pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) }) pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) })
} }
logD("Updated pending playlist: ${_currentPendingPlaylist.value?.preferredName}")
_currentSongsToAdd.value = _currentSongsToAdd.value =
_currentSongsToAdd.value?.let { pendingSongs -> _currentSongsToAdd.value?.let { pendingSongs ->
pendingSongs pendingSongs
@ -91,6 +96,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
.ifEmpty { null } .ifEmpty { null }
.also { refreshChoicesWith = it } .also { refreshChoicesWith = it }
} }
logD("Updated songs to add: ${_currentSongsToAdd.value?.size} songs")
} }
val chosenName = _chosenName.value val chosenName = _chosenName.value
@ -102,6 +108,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
// Nothing to do. // Nothing to do.
} }
} }
logD("Updated chosen name to $chosenName")
refreshChoicesWith = refreshChoicesWith ?: _currentSongsToAdd.value 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. * @param songUids The [Music.UID]s of songs to be present in the playlist.
*/ */
fun setPendingPlaylist(context: Context, songUids: Array<Music.UID>) { fun setPendingPlaylist(context: Context, songUids: Array<Music.UID>) {
val deviceLibrary = musicRepository.deviceLibrary ?: return logD("Opening ${songUids.size} songs to create a playlist from")
val songs = songUids.mapNotNull(deviceLibrary::findSong)
val userLibrary = musicRepository.userLibrary ?: return val userLibrary = musicRepository.userLibrary ?: return
var i = 1 val songs =
while (true) { musicRepository.deviceLibrary
val possibleName = context.getString(R.string.fmt_def_playlist, i) ?.let { songUids.mapNotNull(it::findSong) }
if (userLibrary.playlists.none { it.name.resolve(context) == possibleName }) { ?.also(::refreshPlaylistChoices)
_currentPendingPlaylist.value = PendingPlaylist(possibleName, songs)
return 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. * @param playlistUid The [Music.UID]s of the [Playlist] to rename.
*/ */
fun setPlaylistToRename(playlistUid: Music.UID) { fun setPlaylistToRename(playlistUid: Music.UID) {
logD("Opening playlist $playlistUid to rename")
_currentPlaylistToRename.value = musicRepository.userLibrary?.findPlaylist(playlistUid) _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. * @param playlistUid The [Music.UID] of the [Playlist] to delete.
*/ */
fun setPlaylistToDelete(playlistUid: Music.UID) { fun setPlaylistToDelete(playlistUid: Music.UID) {
logD("Opening playlist $playlistUid to delete")
_currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid) _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. * @param name The new user-inputted name, or null if not present.
*/ */
fun updateChosenName(name: String?) { fun updateChosenName(name: String?) {
logD("Updating chosen name to $name")
_chosenName.value = _chosenName.value =
when { when {
name.isNullOrEmpty() -> ChosenName.Empty name.isNullOrEmpty() -> {
name.isBlank() -> ChosenName.Blank logE("Chosen name is empty")
ChosenName.Empty
}
name.isBlank() -> {
logE("Chosen name is blank")
ChosenName.Blank
}
else -> { else -> {
val trimmed = name.trim() val trimmed = name.trim()
val userLibrary = musicRepository.userLibrary val userLibrary = musicRepository.userLibrary
if (userLibrary != null && userLibrary.findPlaylist(trimmed) == null) { if (userLibrary != null && userLibrary.findPlaylist(trimmed) == null) {
logD("Chosen name is valid")
ChosenName.Valid(trimmed) ChosenName.Valid(trimmed)
} else { } else {
logD("Chosen name already exists in library")
ChosenName.AlreadyExists(trimmed) 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. * @param songUids The [Music.UID]s of songs to add to a playlist.
*/ */
fun setSongsToAdd(songUids: Array<Music.UID>) { fun setSongsToAdd(songUids: Array<Music.UID>) {
val deviceLibrary = musicRepository.deviceLibrary ?: return logD("Opening ${songUids.size} songs to add to a playlist")
val songs = songUids.mapNotNull(deviceLibrary::findSong) _currentSongsToAdd.value =
_currentSongsToAdd.value = songs musicRepository.deviceLibrary
refreshPlaylistChoices(songs) ?.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<Song>) { private fun refreshPlaylistChoices(songs: List<Song>) {
val userLibrary = musicRepository.userLibrary ?: return val userLibrary = musicRepository.userLibrary ?: return
logD("Refreshing playlist choices")
_playlistAddChoices.value = _playlistAddChoices.value =
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map { Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map {
val songSet = it.songs.toSet() val songSet = it.songs.toSet()

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -86,7 +87,9 @@ class RenamePlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding
} }
if (!initializedField) { if (!initializedField) {
requireBinding().playlistName.setText(playlist.name.resolve(requireContext())) val default = playlist.name.resolve(requireContext())
logD("Name input is not initialized, setting to $default")
requireBinding().playlistName.setText(default)
initializedField = true initializedField = true
} }
} }

View file

@ -124,6 +124,7 @@ class IndexerService :
// --- CONTROLLER CALLBACKS --- // --- CONTROLLER CALLBACKS ---
override fun requestIndex(withCache: Boolean) { override fun requestIndex(withCache: Boolean) {
logD("Starting new indexing job")
// Cancel the previous music loading job. // Cancel the previous music loading job.
currentIndexJob?.cancel() currentIndexJob?.cancel()
// Start a new music loading job on a co-routine. // Start a new music loading job on a co-routine.
@ -137,6 +138,7 @@ class IndexerService :
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
val deviceLibrary = musicRepository.deviceLibrary ?: return val deviceLibrary = musicRepository.deviceLibrary ?: return
logD("Music changed, updating shared objects")
// Wipe possibly-invalidated outdated covers // Wipe possibly-invalidated outdated covers
imageLoader.memoryCache?.clear() imageLoader.memoryCache?.clear()
// Clear invalid models from PlaybackStateManager. This is not connected // Clear invalid models from PlaybackStateManager. This is not connected
@ -192,11 +194,14 @@ class IndexerService :
// and thus the music library will not be updated at all. // and thus the music library will not be updated at all.
// TODO: Assuming I unify this with PlaybackService, it's possible that I won't need // TODO: Assuming I unify this with PlaybackService, it's possible that I won't need
// this anymore, or at least I only have to use it when the app task is not removed. // this anymore, or at least I only have to use it when the app task is not removed.
logD("Need to observe, staying in foreground")
if (!foregroundManager.tryStartForeground(observingNotification)) { if (!foregroundManager.tryStartForeground(observingNotification)) {
logD("Notification changed, re-posting notification")
observingNotification.post() observingNotification.post()
} }
} else { } else {
// Not observing and done loading, exit foreground. // Not observing and done loading, exit foreground.
logD("Exiting foreground")
foregroundManager.tryStopForeground() foregroundManager.tryStopForeground()
} }
// Release our wake lock (if we were using it) // Release our wake lock (if we were using it)
@ -237,6 +242,7 @@ class IndexerService :
// setting changed. In such a case, the state will still be updated when // setting changed. In such a case, the state will still be updated when
// the music loading process ends. // the music loading process ends.
if (currentIndexJob == null) { if (currentIndexJob == null) {
logD("Not loading, updating idle session")
updateIdleSession() updateIdleSession()
} }
} }
@ -274,6 +280,7 @@ class IndexerService :
// Check here if we should even start a reindex. This is much less bug-prone than // Check here if we should even start a reindex. This is much less bug-prone than
// registering and de-registering this component as this setting changes. // registering and de-registering this component as this setting changes.
if (musicSettings.shouldBeObserving) { if (musicSettings.shouldBeObserving) {
logD("MediaStore changed, starting re-index")
requestIndex(true) requestIndex(true)
} }
} }

View file

@ -67,6 +67,8 @@ private constructor(
return hashCode return hashCode
} }
override fun toString() = "Playlist(uid=$uid, name=$name)"
companion object { companion object {
/** /**
* Create a new instance with a novel UID. * Create a new instance with a novel UID.

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.music.user package org.oxycblt.auxio.music.user
import java.lang.Exception
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
@ -26,6 +27,8 @@ import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
/** /**
* Organized library information controlled by the user. * Organized library information controlled by the user.
@ -122,7 +125,14 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus
UserLibrary.Factory { UserLibrary.Factory {
override suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary { override suspend fun read(deviceLibraryChannel: Channel<DeviceLibrary>): MutableUserLibrary {
// While were waiting for the library, read our playlists out. // 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() val deviceLibrary = deviceLibraryChannel.receive()
// Convert the database playlist information to actual usable playlists. // Convert the database playlist information to actual usable playlists.
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>() val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
@ -139,6 +149,8 @@ private class UserLibraryImpl(
private val playlistMap: MutableMap<Music.UID, PlaylistImpl>, private val playlistMap: MutableMap<Music.UID, PlaylistImpl>,
private val musicSettings: MusicSettings private val musicSettings: MusicSettings
) : MutableUserLibrary { ) : MutableUserLibrary {
override fun toString() = "UserLibrary(playlists=${playlists.size})"
override val playlists: List<Playlist> override val playlists: List<Playlist>
get() = playlistMap.values.toList() get() = playlistMap.values.toList()
@ -153,34 +165,74 @@ private class UserLibraryImpl(
RawPlaylist( RawPlaylist(
PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw), PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw),
playlistImpl.songs.map { PlaylistSong(it.uid) }) 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) { override suspend fun renamePlaylist(playlist: Playlist, name: String) {
val playlistImpl = val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" } requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(name, musicSettings) } 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) { override suspend fun deletePlaylist(playlist: Playlist) {
synchronized(this) { val playlistImpl =
requireNotNull(playlistMap.remove(playlist.uid)) { "Cannot remove invalid playlist" } 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<Song>) { override suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>) {
val playlistImpl = val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" } requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) } } 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<Song>) { override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) {
val playlistImpl = val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" } requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" }
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(songs) } 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
}
} }
} }

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.util.logD
* *
* @author Alexander Capehart (OxygenCobalt) * @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() { class NavigationViewModel : ViewModel() {
private val _mainNavigationAction = MutableEvent<MainNavigationAction>() private val _mainNavigationAction = MutableEvent<MainNavigationAction>()
@ -96,6 +96,7 @@ class NavigationViewModel : ViewModel() {
* dialog will be shown. * dialog will be shown.
*/ */
fun exploreNavigateToParentArtist(song: Song) { fun exploreNavigateToParentArtist(song: Song) {
logD("Navigating to parent artist of $song")
exploreNavigateToParentArtistImpl(song, song.artists) exploreNavigateToParentArtistImpl(song, song.artists)
} }
@ -106,6 +107,7 @@ class NavigationViewModel : ViewModel() {
* dialog will be shown. * dialog will be shown.
*/ */
fun exploreNavigateToParentArtist(album: Album) { fun exploreNavigateToParentArtist(album: Album) {
logD("Navigating to parent artist of $album")
exploreNavigateToParentArtistImpl(album, album.artists) exploreNavigateToParentArtistImpl(album, album.artists)
} }

View file

@ -78,7 +78,7 @@ class NavigateToArtistDialog :
override fun onDestroyBinding(binding: DialogMusicChoicesBinding) { override fun onDestroyBinding(binding: DialogMusicChoicesBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
choiceAdapter binding.choiceRecycler.adapter = null
} }
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) { override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
/** /**
* A [ViewModel] that stores the current information required for navigation picker dialogs * 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 else -> null
} }
logD("Updated artist choices: ${_artistChoices.value}")
} }
override fun onCleared() { 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]. * @param itemUid The [Music.UID] of the item to show. Must be a [Song] or [Album].
*/ */
fun setArtistChoiceUid(itemUid: Music.UID) { fun setArtistChoiceUid(itemUid: Music.UID) {
logD("Opening navigation choices for $itemUid")
// Support Songs and Albums, which have parent artists. // Support Songs and Albums, which have parent artists.
_artistChoices.value = _artistChoices.value =
when (val music = musicRepository.find(itemUid)) { when (val music = musicRepository.find(itemUid)) {
is Song -> SongArtistNavigationChoices(music) is Song -> {
is Album -> AlbumArtistNavigationChoices(music) logD("Creating navigation choices for song")
else -> null SongArtistNavigationChoices(music)
}
is Album -> {
logD("Creating navigation choices for album")
AlbumArtistNavigationChoices(music)
}
else -> {
logD("Given song/album UID was invalid")
null
}
} }
} }
} }

View file

@ -34,6 +34,7 @@ import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.logD
/** /**
* A [ViewBindingFragment] that shows the current playback state in a compact manner. * A [ViewBindingFragment] that shows the current playback state in a compact manner.
@ -93,6 +94,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, actionMode: ActionMode) { private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, actionMode: ActionMode) {
when (actionMode) { when (actionMode) {
ActionMode.NEXT -> { ActionMode.NEXT -> {
logD("Setting up skip next action")
binding.playbackSecondaryAction.apply { binding.playbackSecondaryAction.apply {
setIconResource(R.drawable.ic_skip_next_24) setIconResource(R.drawable.ic_skip_next_24)
contentDescription = getString(R.string.desc_skip_next) contentDescription = getString(R.string.desc_skip_next)
@ -101,6 +103,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
} }
} }
ActionMode.REPEAT -> { ActionMode.REPEAT -> {
logD("Setting up repeat mode action")
binding.playbackSecondaryAction.apply { binding.playbackSecondaryAction.apply {
contentDescription = getString(R.string.desc_change_repeat) contentDescription = getString(R.string.desc_change_repeat)
iconTint = context.getColorCompat(R.color.sel_activatable_icon) iconTint = context.getColorCompat(R.color.sel_activatable_icon)
@ -109,6 +112,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
} }
} }
ActionMode.SHUFFLE -> { ActionMode.SHUFFLE -> {
logD("Setting up shuffle action")
binding.playbackSecondaryAction.apply { binding.playbackSecondaryAction.apply {
setIconResource(R.drawable.sel_shuffle_state_24) setIconResource(R.drawable.sel_shuffle_state_24)
contentDescription = getString(R.string.desc_shuffle) contentDescription = getString(R.string.desc_shuffle)
@ -121,14 +125,17 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
} }
private fun updateSong(song: Song?) { private fun updateSong(song: Song?) {
if (song != null) { if (song == null) {
val context = requireContext() // Nothing to do.
val binding = requireBinding() return
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()
} }
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) { private fun updatePlaying(isPlaying: Boolean) {

View file

@ -43,6 +43,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.playback.ui.StyledSeekBar
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.share
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -142,6 +143,7 @@ class PlaybackPanelFragment :
when (item.itemId) { when (item.itemId) {
R.id.action_open_equalizer -> { R.id.action_open_equalizer -> {
// Launch the system equalizer app, if possible. // Launch the system equalizer app, if possible.
logD("Launching equalizer")
val equalizerIntent = val equalizerIntent =
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
// Provide audio session ID so the equalizer can show options for this app // Provide audio session ID so the equalizer can show options for this app
@ -200,6 +202,7 @@ class PlaybackPanelFragment :
val binding = requireBinding() val binding = requireBinding()
val context = requireContext() val context = requireContext()
logD("Updating song display: $song")
binding.playbackCover.bind(song) binding.playbackCover.bind(song)
binding.playbackSong.text = song.name.resolve(context) binding.playbackSong.text = song.name.resolve(context)
binding.playbackArtist.text = song.artists.resolveNames(context) binding.playbackArtist.text = song.artists.resolveNames(context)

View file

@ -198,8 +198,14 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont
when (key) { when (key) {
getString(R.string.set_key_replay_gain), getString(R.string.set_key_replay_gain),
getString(R.string.set_key_pre_amp_with), getString(R.string.set_key_pre_amp_with),
getString(R.string.set_key_pre_amp_without) -> listener.onReplayGainSettingsChanged() getString(R.string.set_key_pre_amp_without) -> {
getString(R.string.set_key_notif_action) -> listener.onNotificationActionChanged() logD("Dispatching ReplayGain setting change")
listener.onReplayGainSettingsChanged()
}
getString(R.string.set_key_notif_action) -> {
logD("Dispatching notification setting change")
listener.onNotificationActionChanged()
}
} }
} }

View file

@ -43,6 +43,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent 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. * An [ViewModel] that provides a safe UI frontend for the current playback state.
@ -124,27 +125,32 @@ constructor(
} }
override fun onIndexMoved(queue: Queue) { override fun onIndexMoved(queue: Queue) {
logD("Index moved, updating current song")
_song.value = queue.currentSong _song.value = queue.currentSong
} }
override fun onQueueChanged(queue: Queue, change: Queue.Change) { override fun onQueueChanged(queue: Queue, change: Queue.Change) {
// Other types of queue changes preserve the current song. // Other types of queue changes preserve the current song.
if (change.type == Queue.Change.Type.SONG) { if (change.type == Queue.Change.Type.SONG) {
logD("Queue changed, updating current song")
_song.value = queue.currentSong _song.value = queue.currentSong
} }
} }
override fun onQueueReordered(queue: Queue) { override fun onQueueReordered(queue: Queue) {
logD("Queue completely changed, updating current song")
_isShuffled.value = queue.isShuffled _isShuffled.value = queue.isShuffled
} }
override fun onNewPlayback(queue: Queue, parent: MusicParent?) { override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
logD("New playback started, updating playback information")
_song.value = queue.currentSong _song.value = queue.currentSong
_parent.value = parent _parent.value = parent
_isShuffled.value = queue.isShuffled _isShuffled.value = queue.isShuffled
} }
override fun onStateChanged(state: InternalPlayer.State) { override fun onStateChanged(state: InternalPlayer.State) {
logD("Player state changed, starting new position polling")
_isPlaying.value = state.isPlaying _isPlaying.value = state.isPlaying
// Still need to update the position now due to co-routine launch delays // Still need to update the position now due to co-routine launch delays
_positionDs.value = state.calculateElapsedPositionMs().msToDs() _positionDs.value = state.calculateElapsedPositionMs().msToDs()
@ -169,6 +175,7 @@ constructor(
/** Shuffle all songs in the music library. */ /** Shuffle all songs in the music library. */
fun shuffleAll() { fun shuffleAll() {
logD("Shuffling all songs")
playImpl(null, null, true) playImpl(null, null, true)
} }
@ -184,6 +191,7 @@ constructor(
* @param playbackMode The [MusicMode] to play from. * @param playbackMode The [MusicMode] to play from.
*/ */
fun playFrom(song: Song, playbackMode: MusicMode) { fun playFrom(song: Song, playbackMode: MusicMode) {
logD("Playing $song from $playbackMode")
when (playbackMode) { when (playbackMode) {
MusicMode.SONGS -> playImpl(song, null) MusicMode.SONGS -> playImpl(song, null)
MusicMode.ALBUMS -> playImpl(song, song.album) MusicMode.ALBUMS -> playImpl(song, song.album)
@ -202,10 +210,13 @@ constructor(
*/ */
fun playFromArtist(song: Song, artist: Artist? = null) { fun playFromArtist(song: Song, artist: Artist? = null) {
if (artist != null) { if (artist != null) {
logD("Playing $song from $artist")
playImpl(song, artist) playImpl(song, artist)
} else if (song.artists.size == 1) { } else if (song.artists.size == 1) {
logD("$song has one artist, playing from it")
playImpl(song, song.artists[0]) playImpl(song, song.artists[0])
} else { } else {
logD("$song has multiple artists, showing choice dialog")
_artistPlaybackPickerSong.put(song) _artistPlaybackPickerSong.put(song)
} }
} }
@ -219,10 +230,13 @@ constructor(
*/ */
fun playFromGenre(song: Song, genre: Genre? = null) { fun playFromGenre(song: Song, genre: Genre? = null) {
if (genre != null) { if (genre != null) {
logD("Playing $song from $genre")
playImpl(song, genre) playImpl(song, genre)
} else if (song.genres.size == 1) { } else if (song.genres.size == 1) {
logD("$song has one genre, playing from it")
playImpl(song, song.genres[0]) playImpl(song, song.genres[0])
} else { } else {
logD("$song has multiple genres, showing choice dialog")
_genrePlaybackPickerSong.put(song) _genrePlaybackPickerSong.put(song)
} }
} }
@ -234,6 +248,7 @@ constructor(
* @param playlist The [Playlist] to play from. Must be linked to the [Song]. * @param playlist The [Playlist] to play from. Must be linked to the [Song].
*/ */
fun playFromPlaylist(song: Song, playlist: Playlist) { fun playFromPlaylist(song: Song, playlist: Playlist) {
logD("Playing $song from $playlist")
playImpl(song, playlist) playImpl(song, playlist)
} }
@ -242,70 +257,100 @@ constructor(
* *
* @param album The [Album] to play. * @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]. * Play an [Artist].
* *
* @param artist The [Artist] to play. * @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]. * Play a [Genre].
* *
* @param genre The [Genre] to play. * @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]. * Play a [Playlist].
* *
* @param playlist The [Playlist] to play. * @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. * Play a list of [Song]s.
* *
* @param songs The [Song]s to play. * @param songs The [Song]s to play.
*/ */
fun play(songs: List<Song>) = playbackManager.play(null, null, songs, false) fun play(songs: List<Song>) {
logD("Playing ${songs.size} songs")
playbackManager.play(null, null, songs, false)
}
/** /**
* Shuffle an [Album]. * Shuffle an [Album].
* *
* @param album The [Album] to shuffle. * @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]. * Shuffle an [Artist].
* *
* @param artist The [Artist] to shuffle. * @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]. * Shuffle a [Genre].
* *
* @param genre The [Genre] to shuffle. * @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]. * Shuffle a [Playlist].
* *
* @param playlist The [Playlist] to shuffle. * @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. * Shuffle a list of [Song]s.
* *
* @param songs The [Song]s to shuffle. * @param songs The [Song]s to shuffle.
*/ */
fun shuffle(songs: List<Song>) = playbackManager.play(null, null, songs, true) fun shuffle(songs: List<Song>) {
logD("Shuffling ${songs.size} songs")
playbackManager.play(null, null, songs, true)
}
private fun playImpl( private fun playImpl(
song: Song?, song: Song?,
@ -334,6 +379,7 @@ constructor(
* @param action The [InternalPlayer.Action] to perform eventually. * @param action The [InternalPlayer.Action] to perform eventually.
*/ */
fun startAction(action: InternalPlayer.Action) { fun startAction(action: InternalPlayer.Action) {
logD("Starting action $action")
playbackManager.startAction(action) playbackManager.startAction(action)
} }
@ -345,6 +391,7 @@ constructor(
* @param positionDs The position to seek to, in deci-seconds (1/10th of a second). * @param positionDs The position to seek to, in deci-seconds (1/10th of a second).
*/ */
fun seekTo(positionDs: Long) { fun seekTo(positionDs: Long) {
logD("Seeking to ${positionDs}ds")
playbackManager.seekTo(positionDs.dsToMs()) playbackManager.seekTo(positionDs.dsToMs())
} }
@ -352,11 +399,13 @@ constructor(
/** Skip to the next [Song]. */ /** Skip to the next [Song]. */
fun next() { fun next() {
logD("Skipping to next song")
playbackManager.next() playbackManager.next()
} }
/** Skip to the previous [Song]. */ /** Skip to the previous [Song]. */
fun prev() { fun prev() {
logD("Skipping to previous song")
playbackManager.prev() playbackManager.prev()
} }
@ -366,6 +415,7 @@ constructor(
* @param song The [Song] to add. * @param song The [Song] to add.
*/ */
fun playNext(song: Song) { fun playNext(song: Song) {
logD("Playing $song next")
playbackManager.playNext(song) playbackManager.playNext(song)
} }
@ -375,6 +425,7 @@ constructor(
* @param album The [Album] to add. * @param album The [Album] to add.
*/ */
fun playNext(album: Album) { fun playNext(album: Album) {
logD("Playing $album next")
playbackManager.playNext(musicSettings.albumSongSort.songs(album.songs)) playbackManager.playNext(musicSettings.albumSongSort.songs(album.songs))
} }
@ -384,6 +435,7 @@ constructor(
* @param artist The [Artist] to add. * @param artist The [Artist] to add.
*/ */
fun playNext(artist: Artist) { fun playNext(artist: Artist) {
logD("Playing $artist next")
playbackManager.playNext(musicSettings.artistSongSort.songs(artist.songs)) playbackManager.playNext(musicSettings.artistSongSort.songs(artist.songs))
} }
@ -393,6 +445,7 @@ constructor(
* @param genre The [Genre] to add. * @param genre The [Genre] to add.
*/ */
fun playNext(genre: Genre) { fun playNext(genre: Genre) {
logD("Playing $genre next")
playbackManager.playNext(musicSettings.genreSongSort.songs(genre.songs)) playbackManager.playNext(musicSettings.genreSongSort.songs(genre.songs))
} }
@ -402,6 +455,7 @@ constructor(
* @param playlist The [Playlist] to add. * @param playlist The [Playlist] to add.
*/ */
fun playNext(playlist: Playlist) { fun playNext(playlist: Playlist) {
logD("Playing $playlist next")
playbackManager.playNext(playlist.songs) playbackManager.playNext(playlist.songs)
} }
@ -411,6 +465,7 @@ constructor(
* @param songs The [Song]s to add. * @param songs The [Song]s to add.
*/ */
fun playNext(songs: List<Song>) { fun playNext(songs: List<Song>) {
logD("Playing ${songs.size} songs next")
playbackManager.playNext(songs) playbackManager.playNext(songs)
} }
@ -420,6 +475,7 @@ constructor(
* @param song The [Song] to add. * @param song The [Song] to add.
*/ */
fun addToQueue(song: Song) { fun addToQueue(song: Song) {
logD("Adding $song to queue")
playbackManager.addToQueue(song) playbackManager.addToQueue(song)
} }
@ -429,6 +485,7 @@ constructor(
* @param album The [Album] to add. * @param album The [Album] to add.
*/ */
fun addToQueue(album: Album) { fun addToQueue(album: Album) {
logD("Adding $album to queue")
playbackManager.addToQueue(musicSettings.albumSongSort.songs(album.songs)) playbackManager.addToQueue(musicSettings.albumSongSort.songs(album.songs))
} }
@ -438,6 +495,7 @@ constructor(
* @param artist The [Artist] to add. * @param artist The [Artist] to add.
*/ */
fun addToQueue(artist: Artist) { fun addToQueue(artist: Artist) {
logD("Adding $artist to queue")
playbackManager.addToQueue(musicSettings.artistSongSort.songs(artist.songs)) playbackManager.addToQueue(musicSettings.artistSongSort.songs(artist.songs))
} }
@ -447,6 +505,7 @@ constructor(
* @param genre The [Genre] to add. * @param genre The [Genre] to add.
*/ */
fun addToQueue(genre: Genre) { fun addToQueue(genre: Genre) {
logD("Adding $genre to queue")
playbackManager.addToQueue(musicSettings.genreSongSort.songs(genre.songs)) playbackManager.addToQueue(musicSettings.genreSongSort.songs(genre.songs))
} }
@ -456,6 +515,7 @@ constructor(
* @param playlist The [Playlist] to add. * @param playlist The [Playlist] to add.
*/ */
fun addToQueue(playlist: Playlist) { fun addToQueue(playlist: Playlist) {
logD("Adding $playlist to queue")
playbackManager.addToQueue(playlist.songs) playbackManager.addToQueue(playlist.songs)
} }
@ -465,6 +525,7 @@ constructor(
* @param songs The [Song]s to add. * @param songs The [Song]s to add.
*/ */
fun addToQueue(songs: List<Song>) { fun addToQueue(songs: List<Song>) {
logD("Adding ${songs.size} songs to queue")
playbackManager.addToQueue(songs) playbackManager.addToQueue(songs)
} }
@ -472,11 +533,13 @@ constructor(
/** Toggle [isPlaying] (i.e from playing to paused) */ /** Toggle [isPlaying] (i.e from playing to paused) */
fun togglePlaying() { fun togglePlaying() {
logD("Toggling playing state")
playbackManager.setPlaying(!playbackManager.playerState.isPlaying) playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
} }
/** Toggle [isShuffled] (ex. from on to off) */ /** Toggle [isShuffled] (ex. from on to off) */
fun toggleShuffled() { fun toggleShuffled() {
logD("Toggling shuffled state")
playbackManager.reorder(!playbackManager.queue.isShuffled) playbackManager.reorder(!playbackManager.queue.isShuffled)
} }
@ -486,6 +549,7 @@ constructor(
* @see RepeatMode.increment * @see RepeatMode.increment
*/ */
fun toggleRepeatMode() { fun toggleRepeatMode() {
logD("Toggling repeat mode")
playbackManager.repeatMode = playbackManager.repeatMode.increment() 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. * @param onDone Called when the save is completed with true if successful, and false otherwise.
*/ */
fun savePlaybackState(onDone: (Boolean) -> Unit) { fun savePlaybackState(onDone: (Boolean) -> Unit) {
logD("Saving playback state")
viewModelScope.launch { viewModelScope.launch {
onDone(persistenceRepository.saveState(playbackManager.toSavedState())) 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. * @param onDone Called when the wipe is completed with true if successful, and false otherwise.
*/ */
fun wipePlaybackState(onDone: (Boolean) -> Unit) { fun wipePlaybackState(onDone: (Boolean) -> Unit) {
logD("Wiping playback state")
viewModelScope.launch { onDone(persistenceRepository.saveState(null)) } viewModelScope.launch { onDone(persistenceRepository.saveState(null)) }
} }
@ -518,6 +584,7 @@ constructor(
* otherwise. * otherwise.
*/ */
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) { fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
logD("Force-restoring playback state")
viewModelScope.launch { viewModelScope.launch {
val savedState = persistenceRepository.readState() val savedState = persistenceRepository.readState()
if (savedState != null) { if (savedState != null) {

View file

@ -61,7 +61,7 @@ constructor(
heap = queueDao.getHeap() heap = queueDao.getHeap()
mapping = queueDao.getMapping() mapping = queueDao.getMapping()
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to load playback state data") logE("Unable read playback state")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
return null return null
} }
@ -74,7 +74,7 @@ constructor(
} }
val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent } val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent }
logD("Read playback state") logD("Successfully read playback state")
return PlaybackStateManager.SavedState( return PlaybackStateManager.SavedState(
parent = parent, parent = parent,
@ -90,8 +90,6 @@ constructor(
} }
override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean { 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 { try {
playbackStateDao.nukeState() playbackStateDao.nukeState()
queueDao.nukeHeap() queueDao.nukeHeap()
@ -101,7 +99,8 @@ constructor(
logE(e.stackTraceToString()) logE(e.stackTraceToString())
return false return false
} }
logD("Cleared state")
logD("Successfully cleared previous state")
if (state != null) { if (state != null) {
// Transform saved state into raw state, which can then be written to the database. // Transform saved state into raw state, which can then be written to the database.
val playbackState = val playbackState =
@ -118,12 +117,14 @@ constructor(
state.queueState.heap.mapIndexed { i, song -> state.queueState.heap.mapIndexed { i, song ->
QueueHeapItem(i, requireNotNull(song).uid) QueueHeapItem(i, requireNotNull(song).uid)
} }
val mapping = val mapping =
state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed { state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed {
i, i,
pair -> pair ->
QueueMappingItem(i, pair.first, pair.second) QueueMappingItem(i, pair.first, pair.second)
} }
try { try {
playbackStateDao.insertState(playbackState) playbackStateDao.insertState(playbackState)
queueDao.insertHeap(heap) queueDao.insertHeap(heap)
@ -133,8 +134,10 @@ constructor(
logE(e.stackTraceToString()) logE(e.stackTraceToString())
return false return false
} }
logD("Wrote state")
logD("Successfully wrote new state")
} }
return true return true
} }
} }

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -72,6 +73,7 @@ class PlayFromArtistDialog :
if (it != null) { if (it != null) {
choiceAdapter.update(it.artists, UpdateInstructions.Replace(0)) choiceAdapter.update(it.artists, UpdateInstructions.Replace(0))
} else { } else {
logD("No song to show choices for, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
} }
} }

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -72,6 +73,7 @@ class PlayFromGenreDialog :
if (it != null) { if (it != null) {
choiceAdapter.update(it.genres, UpdateInstructions.Replace(0)) choiceAdapter.update(it.genres, UpdateInstructions.Replace(0))
} else { } else {
logD("No song to show choices for, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
} }
} }

View file

@ -27,6 +27,8 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song 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. * 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]. * @param uid The [Music.UID] of the item to show. Must be a [Song].
*/ */
fun setPickerSongUid(uid: Music.UID) { fun setPickerSongUid(uid: Music.UID) {
logD("Opening picker for song $uid")
_currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid) _currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid)
if (_currentPickerSong.value != null) {
logW("Given song UID was invalid")
}
} }
} }

View file

@ -23,8 +23,7 @@ import kotlin.random.nextInt
import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.Queue.Change.Type import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.playback.queue.Queue.SavedState
/** /**
* A heap-backed play queue. * A heap-backed play queue.
@ -176,6 +175,8 @@ class EditableQueue : Queue {
return return
} }
logD("Reordering queue [shuffled=$shuffled]")
if (shuffled) { if (shuffled) {
val trueIndex = val trueIndex =
if (shuffledMapping.isNotEmpty()) { if (shuffledMapping.isNotEmpty()) {
@ -192,7 +193,7 @@ class EditableQueue : Queue {
shuffledMapping.add(0, shuffledMapping.removeAt(shuffledMapping.indexOf(trueIndex))) shuffledMapping.add(0, shuffledMapping.removeAt(shuffledMapping.indexOf(trueIndex)))
index = 0 index = 0
} else if (shuffledMapping.isNotEmpty()) { } 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]) index = orderedMapping.indexOf(shuffledMapping[index])
shuffledMapping = mutableListOf() shuffledMapping = mutableListOf()
} }
@ -206,15 +207,18 @@ class EditableQueue : Queue {
* @return A [Queue.Change] instance that reflects the changes made. * @return A [Queue.Change] instance that reflects the changes made.
*/ */
fun playNext(songs: List<Song>): Queue.Change { fun playNext(songs: List<Song>): Queue.Change {
logD("Adding ${songs.size} songs to the front of the queue")
val heapIndices = songs.map(::addSongToHeap) val heapIndices = songs.map(::addSongToHeap)
if (shuffledMapping.isNotEmpty()) { if (shuffledMapping.isNotEmpty()) {
// Add the new songs in front of the current index in the shuffled mapping and in front // 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. // of the analogous list song in the ordered mapping.
logD("Must append songs to shuffled mapping")
val orderedIndex = orderedMapping.indexOf(shuffledMapping[index]) val orderedIndex = orderedMapping.indexOf(shuffledMapping[index])
orderedMapping.addAll(orderedIndex + 1, heapIndices) orderedMapping.addAll(orderedIndex + 1, heapIndices)
shuffledMapping.addAll(index + 1, heapIndices) shuffledMapping.addAll(index + 1, heapIndices)
} else { } else {
// Add the new song in front of the current index in the ordered mapping. // 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) orderedMapping.addAll(index + 1, heapIndices)
} }
check() check()
@ -229,10 +233,12 @@ class EditableQueue : Queue {
* @return A [Queue.Change] instance that reflects the changes made. * @return A [Queue.Change] instance that reflects the changes made.
*/ */
fun addToQueue(songs: List<Song>): Queue.Change { fun addToQueue(songs: List<Song>): Queue.Change {
logD("Adding ${songs.size} songs to the back of the queue")
val heapIndices = songs.map(::addSongToHeap) val heapIndices = songs.map(::addSongToHeap)
// Can simple append the new songs to the end of both mappings. // Can simple append the new songs to the end of both mappings.
orderedMapping.addAll(heapIndices) orderedMapping.addAll(heapIndices)
if (shuffledMapping.isNotEmpty()) { if (shuffledMapping.isNotEmpty()) {
logD("Appending songs to shuffled mapping")
shuffledMapping.addAll(heapIndices) shuffledMapping.addAll(heapIndices)
} }
check() check()
@ -257,19 +263,33 @@ class EditableQueue : Queue {
orderedMapping.add(dst, orderedMapping.removeAt(src)) orderedMapping.add(dst, orderedMapping.removeAt(src))
} }
val oldIndex = index
when (index) { when (index) {
// We are moving the currently playing song, correct the index to it's new position. // 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. // 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. // 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 -> { else -> {
// Nothing to do. // Nothing to do.
logD("Move preserved index")
check() check()
return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Move(src, dst)) return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Move(src, dst))
} }
} }
logD("Move changed index: $oldIndex -> $index")
check() check()
return Queue.Change(Queue.Change.Type.INDEX, UpdateInstructions.Move(src, dst)) return Queue.Change(Queue.Change.Type.INDEX, UpdateInstructions.Move(src, dst))
} }
@ -298,15 +318,23 @@ class EditableQueue : Queue {
val type = val type =
when { when {
// We just removed the currently playing song. // 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 was ahead of removed song, shift back to preserve consistency.
index > at -> { index > at -> {
logD("Removed before current song, shift back")
index -= 1 index -= 1
Queue.Change.Type.INDEX Queue.Change.Type.INDEX
} }
// Nothing to do // Nothing to do
else -> Queue.Change.Type.MAPPING else -> {
logD("Removal preserved index")
Queue.Change.Type.MAPPING
}
} }
logD("Committing change of type $type")
check() check()
return Queue.Change(type, UpdateInstructions.Remove(at, 1)) 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() heap = savedState.heap.filterNotNull().toMutableList()
orderedMapping = orderedMapping =
savedState.orderedMapping.mapNotNullTo(mutableListOf()) { heapIndex -> savedState.orderedMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
@ -354,6 +384,7 @@ class EditableQueue : Queue {
while (currentSong?.uid != savedState.songUid && index > -1) { while (currentSong?.uid != savedState.songUid && index > -1) {
index-- index--
} }
logD("Corrected index: ${savedState.index} -> $index")
check() check()
} }
@ -373,13 +404,17 @@ class EditableQueue : Queue {
orphanCandidates.add(entry.index) orphanCandidates.add(entry.index)
} }
} }
logD("Found orphans: ${orphanCandidates.map { heap[it] }}")
orphanCandidates.removeAll(currentMapping.toSet()) orphanCandidates.removeAll(currentMapping.toSet())
if (orphanCandidates.isNotEmpty()) { 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. // 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 // Nothing to re-use, add this song to the queue
logD("No orphan could be re-used")
heap.add(song) heap.add(song)
return heap.lastIndex return heap.lastIndex
} }

View file

@ -88,9 +88,13 @@ class QueueAdapter(private val listener: EditClickListListener<Song>) :
// Have to update not only the currently playing item, but also all items marked // Have to update not only the currently playing item, but also all items marked
// as playing. // 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) { if (currentIndex < lastIndex) {
logD("Moved backwards, must update items above last index")
notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_POSITION) notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_POSITION)
} else { } else {
logD("Moved forwards, update items after index")
notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_POSITION) notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_POSITION)
} }
@ -121,6 +125,10 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS
alpha = 0 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 var isFuture: Boolean
get() = binding.songAlbumCover.isEnabled get() = binding.songAlbumCover.isEnabled
set(value) { set(value) {

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
/** /**
* A [ViewBindingFragment] that displays an editable queue. * A [ViewBindingFragment] that displays an editable queue.
@ -122,13 +123,15 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditClickList
// dependent on where we have to scroll to get to the currently playing song. // dependent on where we have to scroll to get to the currently playing song.
if (notInitialized || scrollTo < start) { if (notInitialized || scrollTo < start) {
// We need to scroll upwards, or initialize the scroll, no need to offset // We need to scroll upwards, or initialize the scroll, no need to offset
logD("Not scrolling downwards, no offset needed")
binding.queueRecycler.scrollToPosition(scrollTo) binding.queueRecycler.scrollToPosition(scrollTo)
} else if (scrollTo > end) { } else if (scrollTo > end) {
// We need to scroll downwards, we need to offset by a screen of songs. // 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 // This does have some error due to how many completely visible items on-screen
// can vary. This is considered okay. // can vary. This is considered okay.
binding.queueRecycler.scrollToPosition( val offset = scrollTo + (end - start)
min(queue.lastIndex, scrollTo + (end - start))) logD("Scrolling downwards, offsetting by $offset")
binding.queueRecycler.scrollToPosition(min(queue.lastIndex, offset))
} }
} }
} }

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent 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. * 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) { override fun onIndexMoved(queue: Queue) {
logD("Index moved, synchronizing and scrolling to new position")
_scrollTo.put(queue.index) _scrollTo.put(queue.index)
_index.value = queue.index _index.value = queue.index
} }
override fun onQueueChanged(queue: Queue, change: Queue.Change) { override fun onQueueChanged(queue: Queue, change: Queue.Change) {
// Queue changed trivially due to item mo -> Diff queue, stay at current index. // Queue changed trivially due to item mo -> Diff queue, stay at current index.
logD("Updating queue display")
_queueInstructions.put(change.instructions) _queueInstructions.put(change.instructions)
_queue.value = queue.resolve() _queue.value = queue.resolve()
if (change.type != Queue.Change.Type.MAPPING) { if (change.type != Queue.Change.Type.MAPPING) {
// Index changed, make sure it remains updated without actually scrolling to it. // Index changed, make sure it remains updated without actually scrolling to it.
logD("Index changed with queue, synchronizing new position")
_index.value = queue.index _index.value = queue.index
} }
} }
override fun onQueueReordered(queue: Queue) { override fun onQueueReordered(queue: Queue) {
// Queue changed completely -> Replace queue, update index // Queue changed completely -> Replace queue, update index
logD("Queue changed completely, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0)) _queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(queue.index) _scrollTo.put(queue.index)
_queue.value = queue.resolve() _queue.value = queue.resolve()
@ -84,6 +89,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
override fun onNewPlayback(queue: Queue, parent: MusicParent?) { override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
// Entirely new queue -> Replace queue, update index // Entirely new queue -> Replace queue, update index
logD("New playback, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0)) _queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(queue.index) _scrollTo.put(queue.index)
_queue.value = queue.resolve() _queue.value = queue.resolve()
@ -102,6 +108,10 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
* range. * range.
*/ */
fun goto(adapterIndex: Int) { fun goto(adapterIndex: Int) {
if (adapterIndex !in queue.value.indices) {
return
}
logD("Going to position $adapterIndex in queue")
playbackManager.goto(adapterIndex) playbackManager.goto(adapterIndex)
} }
@ -115,6 +125,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
if (adapterIndex !in queue.value.indices) { if (adapterIndex !in queue.value.indices) {
return return
} }
logD("Removing item $adapterIndex in queue")
playbackManager.removeQueueItem(adapterIndex) 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) { if (adapterFrom !in queue.value.indices || adapterTo !in queue.value.indices) {
return false return false
} }
logD("Moving $adapterFrom to $adapterFrom in queue")
playbackManager.moveQueueItem(adapterFrom, adapterTo) playbackManager.moveQueueItem(adapterFrom, adapterTo)
return true return true
} }

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.logD
/** /**
* aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp]. * aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp].
@ -61,6 +62,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
// settings. After this, the sliders save their own state, so we do not need to // settings. After this, the sliders save their own state, so we do not need to
// do any restore behavior. // do any restore behavior.
val preAmp = playbackSettings.replayGainPreAmp val preAmp = playbackSettings.replayGainPreAmp
logD("Initializing from $preAmp")
binding.withTagsSlider.value = preAmp.with binding.withTagsSlider.value = preAmp.with
binding.withoutTagsSlider.value = preAmp.without binding.withoutTagsSlider.value = preAmp.without
} }

View file

@ -125,14 +125,22 @@ constructor(
when (playbackSettings.replayGainMode) { when (playbackSettings.replayGainMode) {
// User wants track gain to be preferred. Default to album gain only if // User wants track gain to be preferred. Default to album gain only if
// there is no track gain. // 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 // User wants album gain to be preferred. Default to track gain only if
// here is no album gain. // 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. // 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.parent is Album &&
playbackManager.queue.currentSong?.album == playbackManager.parent playbackManager.queue.currentSong?.album == playbackManager.parent
}
} }
val resolvedGain = val resolvedGain =
@ -184,6 +192,7 @@ constructor(
textTags.vorbis[TAG_RG_TRACK_GAIN] textTags.vorbis[TAG_RG_TRACK_GAIN]
?.run { first().parseReplayGainAdjustment() } ?.run { first().parseReplayGainAdjustment() }
?.let { albumGain = it } ?.let { albumGain = it }
// Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the // 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 // 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 // intrinsic to the format to create the normalized adjustment. This is normally the only

View file

@ -24,7 +24,6 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.EditableQueue import org.oxycblt.auxio.playback.queue.EditableQueue
import org.oxycblt.auxio.playback.queue.Queue 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.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
@ -308,8 +307,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
override val queue = EditableQueue() override val queue = EditableQueue()
@Volatile @Volatile
override var parent: MusicParent? = override var parent: MusicParent? = null
null // FIXME: Parent is interpreted wrong when nothing is playing.
private set private set
@Volatile @Volatile
override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0) override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
@ -373,6 +371,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized @Synchronized
override fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean) { override fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean) {
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
logD("Playing $song from $parent in ${queue.size}-song queue [shuffled=$shuffled]")
// Set up parent and queue // Set up parent and queue
this.parent = parent this.parent = parent
this.queue.start(song, queue, shuffled) this.queue.start(song, queue, shuffled)
@ -392,6 +391,9 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
if (!queue.goto(queue.index + 1)) { if (!queue.goto(queue.index + 1)) {
queue.goto(0) queue.goto(0)
play = repeatMode == RepeatMode.ALL play = repeatMode == RepeatMode.ALL
logD("At end of queue, wrapping around to position 0 [play=$play]")
} else {
logD("Moving to next song")
} }
notifyIndexMoved() notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, play) internalPlayer.loadSong(queue.currentSong, play)
@ -400,12 +402,13 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized @Synchronized
override fun prev() { override fun prev() {
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms] // If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
if (internalPlayer.shouldRewindWithPrev) { if (internalPlayer.shouldRewindWithPrev) {
logD("Rewinding current song")
rewind() rewind()
setPlaying(true) setPlaying(true)
} else { } else {
logD("Moving to previous song")
if (!queue.goto(queue.index - 1)) { if (!queue.goto(queue.index - 1)) {
queue.goto(0) queue.goto(0)
} }
@ -418,16 +421,21 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
override fun goto(index: Int) { override fun goto(index: Int) {
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
if (queue.goto(index)) { if (queue.goto(index)) {
logD("Moving to $index")
notifyIndexMoved() notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true) internalPlayer.loadSong(queue.currentSong, true)
} else {
logW("$index was not in bounds, could not move to it")
} }
} }
@Synchronized @Synchronized
override fun playNext(songs: List<Song>) { override fun playNext(songs: List<Song>) {
if (queue.currentSong == null) { if (queue.currentSong == null) {
logD("Nothing playing, short-circuiting to new playback")
play(songs[0], null, songs, false) play(songs[0], null, songs, false)
} else { } else {
logD("Adding ${songs.size} songs to start of queue")
notifyQueueChanged(queue.playNext(songs)) notifyQueueChanged(queue.playNext(songs))
} }
} }
@ -435,8 +443,10 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized @Synchronized
override fun addToQueue(songs: List<Song>) { override fun addToQueue(songs: List<Song>) {
if (queue.currentSong == null) { if (queue.currentSong == null) {
logD("Nothing playing, short-circuiting to new playback")
play(songs[0], null, songs, false) play(songs[0], null, songs, false)
} else { } else {
logD("Adding ${songs.size} songs to end of queue")
notifyQueueChanged(queue.addToQueue(songs)) notifyQueueChanged(queue.addToQueue(songs))
} }
} }
@ -460,6 +470,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized @Synchronized
override fun reorder(shuffled: Boolean) { override fun reorder(shuffled: Boolean) {
logD("Reordering queue [shuffled=$shuffled]")
queue.reorder(shuffled) queue.reorder(shuffled)
notifyQueueReordered() notifyQueueReordered()
} }
@ -504,11 +515,13 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized @Synchronized
override fun setPlaying(isPlaying: Boolean) { override fun setPlaying(isPlaying: Boolean) {
logD("Updating playing state to $isPlaying")
internalPlayer?.setPlaying(isPlaying) internalPlayer?.setPlaying(isPlaying)
} }
@Synchronized @Synchronized
override fun seekTo(positionMs: Long) { override fun seekTo(positionMs: Long) {
logD("Seeking to ${positionMs}ms")
internalPlayer?.seekTo(positionMs) internalPlayer?.seekTo(positionMs)
} }
@ -530,10 +543,11 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
destructive: Boolean destructive: Boolean
) { ) {
if (isInitialized && !destructive) { if (isInitialized && !destructive) {
logW("Already initialized, cannot apply saved state")
return return
} }
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
logD("Restoring state $savedState") logD("Applying state $savedState")
val lastSong = queue.currentSong val lastSong = queue.currentSong
parent = savedState.parent 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 // it be. Specifically done so we don't pause on music updates that don't really change
// what's playing (ex. playlist editing) // what's playing (ex. playlist editing)
if (lastSong != queue.currentSong) { if (lastSong != queue.currentSong) {
logD("Song changed, must reload player")
// Continuing playback while also possibly doing drastic state updates is // Continuing playback while also possibly doing drastic state updates is
// a bad idea, so pause. // a bad idea, so pause.
internalPlayer.loadSong(queue.currentSong, false) internalPlayer.loadSong(queue.currentSong, false)
if (queue.currentSong != null) { 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 // Internal player may have reloaded the media item, re-seek to the previous
// position // position
seekTo(savedState.positionMs) seekTo(savedState.positionMs)
@ -560,36 +576,42 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
// --- CALLBACKS --- // --- CALLBACKS ---
private fun notifyIndexMoved() { private fun notifyIndexMoved() {
logD("Dispatching index change")
for (callback in listeners) { for (callback in listeners) {
callback.onIndexMoved(queue) callback.onIndexMoved(queue)
} }
} }
private fun notifyQueueChanged(change: Queue.Change) { private fun notifyQueueChanged(change: Queue.Change) {
logD("Dispatching queue change $change")
for (callback in listeners) { for (callback in listeners) {
callback.onQueueChanged(queue, change) callback.onQueueChanged(queue, change)
} }
} }
private fun notifyQueueReordered() { private fun notifyQueueReordered() {
logD("Dispatching queue reordering")
for (callback in listeners) { for (callback in listeners) {
callback.onQueueReordered(queue) callback.onQueueReordered(queue)
} }
} }
private fun notifyNewPlayback() { private fun notifyNewPlayback() {
logD("Dispatching new playback")
for (callback in listeners) { for (callback in listeners) {
callback.onNewPlayback(queue, parent) callback.onNewPlayback(queue, parent)
} }
} }
private fun notifyStateChanged() { private fun notifyStateChanged() {
logD("Dispatching player state change")
for (callback in listeners) { for (callback in listeners) {
callback.onStateChanged(playerState) callback.onStateChanged(playerState)
} }
} }
private fun notifyRepeatModeChanged() { private fun notifyRepeatModeChanged() {
logD("Dispatching repeat mode change")
for (callback in listeners) { for (callback in listeners) {
callback.onRepeatChanged(repeatMode) callback.onRepeatChanged(repeatMode)
} }

View file

@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.playback.state.PlaybackStateManager 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]. * 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 // 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 // wrong action at the wrong time will result in the app crashing, and there is
// nothing I can do about it. // nothing I can do about it.
logD("Delivering media button intent $intent")
intent.component = ComponentName(context, PlaybackService::class.java) intent.component = ComponentName(context, PlaybackService::class.java)
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }

View file

@ -86,6 +86,7 @@ constructor(
* @param intent The [Intent.ACTION_MEDIA_BUTTON] [Intent] to forward. * @param intent The [Intent.ACTION_MEDIA_BUTTON] [Intent] to forward.
*/ */
fun handleMediaButtonIntent(intent: Intent) { fun handleMediaButtonIntent(intent: Intent) {
logD("Forwarding $intent to MediaButtonReciever")
MediaButtonReceiver.handleIntent(mediaSession, intent) MediaButtonReceiver.handleIntent(mediaSession, intent)
} }
@ -283,8 +284,10 @@ constructor(
* playback is currently occuring from all songs. * playback is currently occuring from all songs.
*/ */
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) { private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
logD("Updating media metadata to $song with $parent")
if (song == null) { if (song == null) {
// Nothing playing, reset the MediaSession and close the notification. // Nothing playing, reset the MediaSession and close the notification.
logD("Nothing playing, resetting media session")
mediaSession.setMetadata(emptyMetadata) mediaSession.setMetadata(emptyMetadata)
return return
} }
@ -316,12 +319,17 @@ constructor(
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
// These fields are nullable and so we must check first before adding them to the fields. // These fields are nullable and so we must check first before adding them to the fields.
song.track?.let { song.track?.let {
logD("Adding track information")
builder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, it.toLong()) builder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, it.toLong())
} }
song.disc?.let { song.disc?.let {
logD("Adding disc information")
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.number.toLong()) 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 // 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, // nice things we can do like square cropping or high quality covers. Instead,
@ -330,6 +338,8 @@ constructor(
song, song,
object : BitmapProvider.Target { object : BitmapProvider.Target {
override fun onCompleted(bitmap: Bitmap?) { 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_ART, bitmap)
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
val metadata = builder.build() val metadata = builder.build()
@ -364,6 +374,7 @@ constructor(
// playback state. // playback state.
MediaSessionCompat.QueueItem(description, i.toLong()) MediaSessionCompat.QueueItem(description, i.toLong())
} }
logD("Uploading ${queueItems.size} songs to MediaSession queue")
mediaSession.setQueue(queueItems) mediaSession.setQueue(queueItems)
} }
@ -384,7 +395,8 @@ constructor(
// Add the secondary action (either repeat/shuffle depending on the configuration) // Add the secondary action (either repeat/shuffle depending on the configuration)
val secondaryAction = val secondaryAction =
when (playbackSettings.notificationAction) { when (playbackSettings.notificationAction) {
ActionMode.SHUFFLE -> ActionMode.SHUFFLE -> {
logD("Using shuffle MediaSession action")
PlaybackStateCompat.CustomAction.Builder( PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INVERT_SHUFFLE, PlaybackService.ACTION_INVERT_SHUFFLE,
context.getString(R.string.desc_shuffle), context.getString(R.string.desc_shuffle),
@ -393,11 +405,14 @@ constructor(
} else { } else {
R.drawable.ic_shuffle_off_24 R.drawable.ic_shuffle_off_24
}) })
else -> }
else -> {
logD("Using repeat mode MediaSession action")
PlaybackStateCompat.CustomAction.Builder( PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INC_REPEAT_MODE, PlaybackService.ACTION_INC_REPEAT_MODE,
context.getString(R.string.desc_change_repeat), context.getString(R.string.desc_change_repeat),
playbackManager.repeatMode.icon) playbackManager.repeatMode.icon)
}
} }
state.addCustomAction(secondaryAction.build()) state.addCustomAction(secondaryAction.build())
@ -415,14 +430,22 @@ constructor(
/** Invalidate the "secondary" action (i.e shuffle/repeat mode). */ /** Invalidate the "secondary" action (i.e shuffle/repeat mode). */
private fun invalidateSecondaryAction() { private fun invalidateSecondaryAction() {
logD("Invalidating secondary action")
invalidateSessionState() invalidateSessionState()
when (playbackSettings.notificationAction) { when (playbackSettings.notificationAction) {
ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.queue.isShuffled) ActionMode.SHUFFLE -> {
else -> notification.updateRepeatMode(playbackManager.repeatMode) logD("Using shuffle notification action")
notification.updateShuffled(playbackManager.queue.isShuffled)
}
else -> {
logD("Using repeat mode notification action")
notification.updateRepeatMode(playbackManager.repeatMode)
}
} }
if (!bitmapProvider.isBusy) { if (!bitmapProvider.isBusy) {
logD("Not loading a bitmap, post the notification")
listener?.onPostNotification(notification) listener?.onPostNotification(notification)
} }
} }

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.service.ForegroundServiceNotification import org.oxycblt.auxio.service.ForegroundServiceNotification
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newBroadcastPendingIntent import org.oxycblt.auxio.util.newBroadcastPendingIntent
import org.oxycblt.auxio.util.newMainPendingIntent 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. * @param metadata The [MediaMetadataCompat] to display in this notification.
*/ */
fun updateMetadata(metadata: MediaMetadataCompat) { fun updateMetadata(metadata: MediaMetadataCompat) {
logD("Updating shown metadata")
setLargeIcon(metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)) setLargeIcon(metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART))
setContentTitle(metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)) setContentTitle(metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE))
setContentText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ARTIST)) 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. // content text to being above the title. Use an appropriate field for both.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Display description -> Parent in which playback is occurring // Display description -> Parent in which playback is occurring
logD("API 24+, showing parent information")
setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION)) setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION))
} else { } else {
logD("API 24 or lower, showing album information")
setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ALBUM)) 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. * @param isPlaying Whether playback should be indicated as ongoing or paused.
*/ */
fun updatePlaying(isPlaying: Boolean) { fun updatePlaying(isPlaying: Boolean) {
logD("Updating playing state: $isPlaying")
mActions[2] = buildPlayPauseAction(context, isPlaying) mActions[2] = buildPlayPauseAction(context, isPlaying)
} }
@ -102,6 +107,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
* @param repeatMode The current [RepeatMode]. * @param repeatMode The current [RepeatMode].
*/ */
fun updateRepeatMode(repeatMode: RepeatMode) { fun updateRepeatMode(repeatMode: RepeatMode) {
logD("Applying repeat mode action: $repeatMode")
mActions[0] = buildRepeatAction(context, 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. * @param isShuffled Whether the queue is currently shuffled or not.
*/ */
fun updateShuffled(isShuffled: Boolean) { fun updateShuffled(isShuffled: Boolean) {
logD("Applying shuffle action: $isShuffled")
mActions[0] = buildShuffleAction(context, isShuffled) mActions[0] = buildShuffleAction(context, isShuffled)
} }

View file

@ -56,6 +56,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider import org.oxycblt.auxio.widgets.WidgetProvider
@ -243,6 +244,7 @@ class PlaybackService :
} }
override fun setPlaying(isPlaying: Boolean) { override fun setPlaying(isPlaying: Boolean) {
logD("Updating player state to $isPlaying")
player.playWhenReady = isPlaying player.playWhenReady = isPlaying
} }
@ -254,14 +256,17 @@ class PlaybackService :
if (player.playWhenReady) { if (player.playWhenReady) {
// Mark that we have started playing so that the notification can now be posted. // Mark that we have started playing so that the notification can now be posted.
hasPlayed = true hasPlayed = true
logD("Player has started playing")
if (!openAudioEffectSession) { if (!openAudioEffectSession) {
// Convention to start an audioeffect session on play/pause rather than // Convention to start an audioeffect session on play/pause rather than
// start/stop // start/stop
logD("Opening audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = true openAudioEffectSession = true
} }
} else if (openAudioEffectSession) { } else if (openAudioEffectSession) {
// Make sure to close the audio session when we stop playback. // Make sure to close the audio session when we stop playback.
logD("Closing audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = false openAudioEffectSession = false
} }
@ -273,6 +278,7 @@ class PlaybackService :
Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_IS_PLAYING_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY)) { Player.EVENT_POSITION_DISCONTINUITY)) {
logD("Player state changed, must synchronize state")
playbackManager.synchronizeState(this) playbackManager.synchronizeState(this)
} }
} }
@ -281,12 +287,15 @@ class PlaybackService :
if (state == Player.STATE_ENDED) { if (state == Player.STATE_ENDED) {
// Player ended, repeat the current track if we are configured to. // Player ended, repeat the current track if we are configured to.
if (playbackManager.repeatMode == RepeatMode.TRACK) { if (playbackManager.repeatMode == RepeatMode.TRACK) {
logD("Looping current track")
playbackManager.rewind() playbackManager.rewind()
// May be configured to pause when we repeat a track. // May be configured to pause when we repeat a track.
if (playbackSettings.pauseOnRepeat) { if (playbackSettings.pauseOnRepeat) {
logD("Pausing track on loop")
playbackManager.setPlaying(false) playbackManager.setPlaying(false)
} }
} else { } else {
logD("Track ended, moving to next track")
playbackManager.next() playbackManager.next()
} }
} }
@ -295,12 +304,15 @@ class PlaybackService :
override fun onPlayerError(error: PlaybackException) { override fun onPlayerError(error: PlaybackException) {
// TODO: Replace with no skipping and a notification instead // TODO: Replace with no skipping and a notification instead
// If there's any issue, just go to the next song. // If there's any issue, just go to the next song.
logE("Player error occured")
logE(error.stackTraceToString())
playbackManager.next() playbackManager.next()
} }
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.deviceLibrary && musicRepository.deviceLibrary != null) { if (changes.deviceLibrary && musicRepository.deviceLibrary != null) {
// We now have a library, see if we have anything we need to do. // We now have a library, see if we have anything we need to do.
logD("Library obtained, requesting action")
playbackManager.requestAction(this) playbackManager.requestAction(this)
} }
} }
@ -308,6 +320,7 @@ class PlaybackService :
// --- OTHER FUNCTIONS --- // --- OTHER FUNCTIONS ---
private fun broadcastAudioEffectAction(event: String) { private fun broadcastAudioEffectAction(event: String) {
logD("Broadcasting AudioEffect event: $event")
sendBroadcast( sendBroadcast(
Intent(event) Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName) .putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
@ -333,11 +346,10 @@ class PlaybackService :
// No library, cannot do anything. // No library, cannot do anything.
?: return false ?: return false
logD("Performing action: $action")
when (action) { when (action) {
// Restore state -> Start a new restoreState job // Restore state -> Start a new restoreState job
is InternalPlayer.Action.RestoreState -> { is InternalPlayer.Action.RestoreState -> {
logD("Restoring playback state")
restoreScope.launch { restoreScope.launch {
persistenceRepository.readState()?.let { persistenceRepository.readState()?.let {
playbackManager.applySavedState(it, false) playbackManager.applySavedState(it, false)
@ -346,11 +358,13 @@ class PlaybackService :
} }
// Shuffle all -> Start new playback from all songs // Shuffle all -> Start new playback from all songs
is InternalPlayer.Action.ShuffleAll -> { is InternalPlayer.Action.ShuffleAll -> {
logD("Shuffling all tracks")
playbackManager.play( playbackManager.play(
null, null, musicSettings.songSort.songs(deviceLibrary.songs), true) 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 // Open -> Try to find the Song for the given file and then play it from all songs
is InternalPlayer.Action.Open -> { is InternalPlayer.Action.Open -> {
logD("Opening specified file")
deviceLibrary.findSongForUri(application, action.uri)?.let { song -> deviceLibrary.findSongForUri(application, action.uri)?.let { song ->
playbackManager.play( playbackManager.play(
song, song,
@ -371,8 +385,9 @@ class PlaybackService :
// where changing a setting would cause the notification to appear in an unfriendly // where changing a setting would cause the notification to appear in an unfriendly
// manner. // manner.
if (hasPlayed) { if (hasPlayed) {
logD("Updating notification") logD("Played before, starting foreground state")
if (!foregroundManager.tryStartForeground(notification)) { if (!foregroundManager.tryStartForeground(notification)) {
logD("Notification changed, re-posting")
notification.post() notification.post()
} }
} }
@ -397,6 +412,7 @@ class PlaybackService :
// 3. Some internal framework thing that also handles bluetooth headsets // 3. Some internal framework thing that also handles bluetooth headsets
// Just use ACTION_HEADSET_PLUG. // Just use ACTION_HEADSET_PLUG.
AudioManager.ACTION_HEADSET_PLUG -> { AudioManager.ACTION_HEADSET_PLUG -> {
logD("Received headset plug event")
when (intent.getIntExtra("state", -1)) { when (intent.getIntExtra("state", -1)) {
0 -> pauseFromHeadsetPlug() 0 -> pauseFromHeadsetPlug()
1 -> playFromHeadsetPlug() 1 -> playFromHeadsetPlug()
@ -404,21 +420,41 @@ class PlaybackService :
initialHeadsetPlugEventHandled = true initialHeadsetPlugEventHandled = true
} }
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromHeadsetPlug() AudioManager.ACTION_AUDIO_BECOMING_NOISY -> {
logD("Received Headset noise event")
pauseFromHeadsetPlug()
}
// --- AUXIO EVENTS --- // --- AUXIO EVENTS ---
ACTION_PLAY_PAUSE -> ACTION_PLAY_PAUSE -> {
logD("Received play event")
playbackManager.setPlaying(!playbackManager.playerState.isPlaying) playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
ACTION_INC_REPEAT_MODE -> }
ACTION_INC_REPEAT_MODE -> {
logD("Received repeat mode event")
playbackManager.repeatMode = playbackManager.repeatMode.increment() playbackManager.repeatMode = playbackManager.repeatMode.increment()
ACTION_INVERT_SHUFFLE -> playbackManager.reorder(!playbackManager.queue.isShuffled) }
ACTION_SKIP_PREV -> playbackManager.prev() ACTION_INVERT_SHUFFLE -> {
ACTION_SKIP_NEXT -> playbackManager.next() 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 -> { ACTION_EXIT -> {
logD("Received exit event")
playbackManager.setPlaying(false) playbackManager.setPlaying(false)
stopAndSave() stopAndSave()
} }
WidgetProvider.ACTION_WIDGET_UPDATE -> widgetComponent.update() WidgetProvider.ACTION_WIDGET_UPDATE -> {
logD("Received widget update event")
widgetComponent.update()
}
} }
} }

View file

@ -25,6 +25,7 @@ import com.google.android.material.button.MaterialButton
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.RippleFixMaterialButton import org.oxycblt.auxio.ui.RippleFixMaterialButton
import org.oxycblt.auxio.util.getInteger 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 * 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 val targetRadius = if (activated) 0.3f else 0.5f
if (!isLaidOut) { if (!isLaidOut) {
// Not laid out, initialize it without animation before drawing. // Not laid out, initialize it without animation before drawing.
logD("Not laid out, immediately updating corner radius")
updateCornerRadiusRatio(targetRadius) updateCornerRadiusRatio(targetRadius)
return return
} }
logD("Starting corner radius animation")
animator?.cancel() animator?.cancel()
animator = animator =
ValueAnimator.ofFloat(currentCornerRadiusRatio, targetRadius).apply { ValueAnimator.ofFloat(currentCornerRadiusRatio, targetRadius).apply {

View file

@ -81,6 +81,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
// zero, use 1 instead and disable the SeekBar. // zero, use 1 instead and disable the SeekBar.
val to = max(value, 1) val to = max(value, 1)
isEnabled = value > 0 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 // 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. // down so that we don't crash and instead have an annoying visual flicker.
if (positionDs > to) { if (positionDs > to) {

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.util.logD
/** /**
* Implements the fuzzy-ish searching algorithm used in the search view. * 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) : class SearchEngineImpl @Inject constructor(@ApplicationContext private val context: Context) :
SearchEngine { SearchEngine {
override suspend fun search(items: SearchEngine.Items, query: String) = override suspend fun search(items: SearchEngine.Items, query: String): SearchEngine.Items {
SearchEngine.Items( logD("Launching search for $query")
return SearchEngine.Items(
songs = songs =
items.songs?.searchListImpl(query) { q, song -> items.songs?.searchListImpl(query) { q, song ->
song.path.name.contains(q, ignoreCase = true) song.path.name.contains(q, ignoreCase = true)
@ -75,6 +77,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte
artists = items.artists?.searchListImpl(query), artists = items.artists?.searchListImpl(query),
genres = items.genres?.searchListImpl(query), genres = items.genres?.searchListImpl(query),
playlists = items.playlists?.searchListImpl(query)) playlists = items.playlists?.searchListImpl(query))
}
/** /**
* Search a given [Music] list. * Search a given [Music] list.

View file

@ -115,6 +115,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
if (!launchedKeyboard) { if (!launchedKeyboard) {
// Auto-open the keyboard when this view is shown // Auto-open the keyboard when this view is shown
this@SearchFragment.logD("Keyboard is not shown yet")
showKeyboard(this) showKeyboard(this)
launchedKeyboard = true launchedKeyboard = true
} }
@ -155,6 +156,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
if (item.itemId != R.id.submenu_filtering) { if (item.itemId != R.id.submenu_filtering) {
// Is a change in filter mode and not just a junk submenu click, update // Is a change in filter mode and not just a junk submenu click, update
// the filtering within SearchViewModel. // the filtering within SearchViewModel.
logD("Filter mode selected")
item.isChecked = true item.isChecked = true
searchModel.setFilterOptionId(item.itemId) searchModel.setFilterOptionId(item.itemId)
return true return true
@ -189,6 +191,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
// I would make it so that the position is only scrolled back to the top when // 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 // the query actually changes instead of once every re-creation event, but sadly
// that doesn't seem possible. // that doesn't seem possible.
logD("Update finished, scrolling to top")
binding.searchRecycler.scrollToPosition(0) binding.searchRecycler.scrollToPosition(0)
} }
} }
@ -233,6 +236,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
* @param view The [View] to focus the keyboard on. * @param view The [View] to focus the keyboard on.
*/ */
private fun showKeyboard(view: View) { private fun showKeyboard(view: View) {
logD("Launching keyboard")
view.apply { view.apply {
requestFocus() requestFocus()
postDelayed(200) { postDelayed(200) {
@ -244,6 +248,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
/** Safely hide the keyboard from this view. */ /** Safely hide the keyboard from this view. */
private fun hideKeyboard() { private fun hideKeyboard() {
logD("Hiding keyboard")
requireNotNull(imm) { "InputMethodManager was not available" } requireNotNull(imm) { "InputMethodManager was not available" }
.hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) .hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
} }

View file

@ -78,6 +78,7 @@ constructor(
override fun onMusicChanges(changes: MusicRepository.Changes) { override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.deviceLibrary || changes.userLibrary) { if (changes.deviceLibrary || changes.userLibrary) {
logD("Music changed, re-searching library")
search(lastQuery) search(lastQuery)
} }
} }
@ -96,14 +97,13 @@ constructor(
val deviceLibrary = musicRepository.deviceLibrary val deviceLibrary = musicRepository.deviceLibrary
val userLibrary = musicRepository.userLibrary val userLibrary = musicRepository.userLibrary
if (query.isNullOrEmpty() || deviceLibrary == null || userLibrary == null) { if (query.isNullOrEmpty() || deviceLibrary == null || userLibrary == null) {
logD("Search query is not applicable.") logD("Cannot search for the current query, aborting")
_searchResults.value = listOf() _searchResults.value = listOf()
return return
} }
logD("Searching music library for $query")
// Searching is time-consuming, so do it in the background. // Searching is time-consuming, so do it in the background.
logD("Searching music library for $query")
currentSearchJob = currentSearchJob =
viewModelScope.launch { viewModelScope.launch {
_searchResults.value = _searchResults.value =
@ -121,6 +121,7 @@ constructor(
val items = val items =
if (filterMode == null) { if (filterMode == null) {
// A nulled filter mode means to not filter anything. // A nulled filter mode means to not filter anything.
logD("No filter mode specified, using entire library")
SearchEngine.Items( SearchEngine.Items(
deviceLibrary.songs, deviceLibrary.songs,
deviceLibrary.albums, deviceLibrary.albums,
@ -128,6 +129,7 @@ constructor(
deviceLibrary.genres, deviceLibrary.genres,
userLibrary.playlists) userLibrary.playlists)
} else { } else {
logD("Filter mode specified, filtering library")
SearchEngine.Items( SearchEngine.Items(
songs = if (filterMode == MusicMode.SONGS) deviceLibrary.songs else null, songs = if (filterMode == MusicMode.SONGS) deviceLibrary.songs else null,
albums = if (filterMode == MusicMode.ALBUMS) deviceLibrary.albums else null, albums = if (filterMode == MusicMode.ALBUMS) deviceLibrary.albums else null,
@ -141,11 +143,13 @@ constructor(
return buildList { return buildList {
results.artists?.let { results.artists?.let {
logD("Adding ${it.size} artists to search results")
val header = BasicHeader(R.string.lbl_artists) val header = BasicHeader(R.string.lbl_artists)
add(header) add(header)
addAll(SORT.artists(it)) addAll(SORT.artists(it))
} }
results.albums?.let { results.albums?.let {
logD("Adding ${it.size} albums to search results")
val header = BasicHeader(R.string.lbl_albums) val header = BasicHeader(R.string.lbl_albums)
if (isNotEmpty()) { if (isNotEmpty()) {
add(Divider(header)) add(Divider(header))
@ -155,6 +159,7 @@ constructor(
addAll(SORT.albums(it)) addAll(SORT.albums(it))
} }
results.playlists?.let { results.playlists?.let {
logD("Adding ${it.size} playlists to search results")
val header = BasicHeader(R.string.lbl_playlists) val header = BasicHeader(R.string.lbl_playlists)
if (isNotEmpty()) { if (isNotEmpty()) {
add(Divider(header)) add(Divider(header))
@ -164,6 +169,7 @@ constructor(
addAll(SORT.playlists(it)) addAll(SORT.playlists(it))
} }
results.genres?.let { results.genres?.let {
logD("Adding ${it.size} genres to search results")
val header = BasicHeader(R.string.lbl_genres) val header = BasicHeader(R.string.lbl_genres)
if (isNotEmpty()) { if (isNotEmpty()) {
add(Divider(header)) add(Divider(header))
@ -173,6 +179,7 @@ constructor(
addAll(SORT.genres(it)) addAll(SORT.genres(it))
} }
results.songs?.let { results.songs?.let {
logD("Adding ${it.size} songs to search results")
val header = BasicHeader(R.string.lbl_songs) val header = BasicHeader(R.string.lbl_songs)
if (isNotEmpty()) { if (isNotEmpty()) {
add(Divider(header)) add(Divider(header))

View file

@ -108,6 +108,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
// Android 11 seems to now handle the app chooser situations on its own now // 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 // [along with adding a new permission that breaks the old manual code], so
// we just do a typical activity launch. // we just do a typical activity launch.
logD("Using API 30+ chooser")
try { try {
context.startActivity(browserIntent) context.startActivity(browserIntent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
@ -119,6 +120,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
// not work in all cases, especially when no default app was set. If that is the // 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 // case, we will try to manually handle these cases before we try to launch the
// browser. // browser.
logD("Resolving browser activity for chooser")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val pkgName = val pkgName =
context.packageManager context.packageManager
@ -128,16 +130,17 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
if (pkgName != null) { if (pkgName != null) {
if (pkgName == "android") { if (pkgName == "android") {
// No default browser [Must open app chooser, may not be supported] // No default browser [Must open app chooser, may not be supported]
logD("No default browser found")
openAppChooser(browserIntent) openAppChooser(browserIntent)
} else } else logD("Opening browser intent")
try { try {
browserIntent.setPackage(pkgName) browserIntent.setPackage(pkgName)
startActivity(browserIntent) startActivity(browserIntent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
// Not a browser but an app chooser // Not a browser but an app chooser
browserIntent.setPackage(null) browserIntent.setPackage(null)
openAppChooser(browserIntent) openAppChooser(browserIntent)
} }
} else { } else {
// No app installed to open the link // No app installed to open the link
context.showToast(R.string.err_no_app) context.showToast(R.string.err_no_app)
@ -151,6 +154,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
* @param intent The [Intent] to show an app chooser for. * @param intent The [Intent] to show an app chooser for.
*/ */
private fun openAppChooser(intent: Intent) { private fun openAppChooser(intent: Intent) {
logD("Opening app chooser for ${intent.action}")
val chooserIntent = val chooserIntent =
Intent(Intent.ACTION_CHOOSER) Intent(Intent.ACTION_CHOOSER)
.putExtra(Intent.EXTRA_INTENT, intent) .putExtra(Intent.EXTRA_INTENT, intent)

View file

@ -107,9 +107,10 @@ abstract class BasePreferenceFragment(@XmlRes private val screen: Int) :
when (preference) { when (preference) {
is IntListPreference -> { is IntListPreference -> {
// Copy the built-in preference dialog launching code into our project so // 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) val dialog = IntListPreferenceDialog.from(preference)
dialog.setTargetFragment(this, 0) @Suppress("DEPRECATION") dialog.setTargetFragment(this, 0)
dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG) dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG)
} }
is WrappedDialogPreference -> { is WrappedDialogPreference -> {
@ -128,6 +129,7 @@ abstract class BasePreferenceFragment(@XmlRes private val screen: Int) :
} }
if (preference is PreferenceCategory) { if (preference is PreferenceCategory) {
// Recurse into preference children to make sure they are set up as well
preference.children.forEach(::setupPreference) preference.children.forEach(::setupPreference)
return return
} }

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -64,18 +65,22 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
// do one. // do one.
when (preference.key) { when (preference.key) {
getString(R.string.set_key_ui) -> { getString(R.string.set_key_ui) -> {
logD("Navigating to UI preferences")
findNavController() findNavController()
.navigateSafe(RootPreferenceFragmentDirections.goToUiPreferences()) .navigateSafe(RootPreferenceFragmentDirections.goToUiPreferences())
} }
getString(R.string.set_key_personalize) -> { getString(R.string.set_key_personalize) -> {
logD("Navigating to personalization preferences")
findNavController() findNavController()
.navigateSafe(RootPreferenceFragmentDirections.goToPersonalizePreferences()) .navigateSafe(RootPreferenceFragmentDirections.goToPersonalizePreferences())
} }
getString(R.string.set_key_music) -> { getString(R.string.set_key_music) -> {
logD("Navigating to music preferences")
findNavController() findNavController()
.navigateSafe(RootPreferenceFragmentDirections.goToMusicPreferences()) .navigateSafe(RootPreferenceFragmentDirections.goToMusicPreferences())
} }
getString(R.string.set_key_audio) -> { getString(R.string.set_key_audio) -> {
logD("Navigating to audio preferences")
findNavController() findNavController()
.navigateSafe(RootPreferenceFragmentDirections.goToAudioPreferences()) .navigateSafe(RootPreferenceFragmentDirections.goToAudioPreferences())
} }
@ -85,6 +90,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
playbackModel.savePlaybackState { saved -> playbackModel.savePlaybackState { saved ->
// Use the nullable context, as we could try to show a toast when this // Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached. // fragment is no longer attached.
logD("Showing saving confirmation")
if (saved) { if (saved) {
context?.showToast(R.string.lbl_state_saved) context?.showToast(R.string.lbl_state_saved)
} else { } else {
@ -94,6 +100,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
} }
getString(R.string.set_key_wipe_state) -> { getString(R.string.set_key_wipe_state) -> {
playbackModel.wipePlaybackState { wiped -> playbackModel.wipePlaybackState { wiped ->
logD("Showing wipe confirmation")
if (wiped) { if (wiped) {
// Use the nullable context, as we could try to show a toast when this // Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached. // fragment is no longer attached.
@ -105,6 +112,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
} }
getString(R.string.set_key_restore_state) -> getString(R.string.set_key_restore_state) ->
playbackModel.tryRestorePlaybackState { restored -> playbackModel.tryRestorePlaybackState { restored ->
logD("Showing restore confirmation")
if (restored) { if (restored) {
// Use the nullable context, as we could try to show a toast when this // Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached. // fragment is no longer attached.

View file

@ -22,6 +22,7 @@ import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.BasePreferenceFragment
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
/** /**
@ -33,6 +34,7 @@ class AudioPreferenceFragment : BasePreferenceFragment(R.xml.preferences_audio)
override fun onOpenDialogPreference(preference: WrappedDialogPreference) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) {
if (preference.key == getString(R.string.set_key_pre_amp)) { if (preference.key == getString(R.string.set_key_pre_amp)) {
logD("Navigating to pre-amp dialog")
findNavController().navigateSafe(AudioPreferenceFragmentDirections.goToPreAmpDialog()) findNavController().navigateSafe(AudioPreferenceFragmentDirections.goToPreAmpDialog())
} }
} }

View file

@ -26,6 +26,7 @@ import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.BasePreferenceFragment
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
/** /**
@ -39,6 +40,7 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
override fun onOpenDialogPreference(preference: WrappedDialogPreference) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) {
if (preference.key == getString(R.string.set_key_separators)) { if (preference.key == getString(R.string.set_key_separators)) {
logD("Navigating to separator dialog")
findNavController() findNavController()
.navigateSafe(MusicPreferenceFragmentDirections.goToSeparatorsDialog()) .navigateSafe(MusicPreferenceFragmentDirections.goToSeparatorsDialog())
} }
@ -46,8 +48,10 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
override fun onSetupPreference(preference: Preference) { override fun onSetupPreference(preference: Preference) {
if (preference.key == getString(R.string.set_key_cover_mode)) { if (preference.key == getString(R.string.set_key_cover_mode)) {
logD("Configuring cover mode setting")
preference.onPreferenceChangeListener = preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ -> Preference.OnPreferenceChangeListener { _, _ ->
logD("Cover mode changed, resetting image memory cache")
imageLoader.memoryCache?.clear() imageLoader.memoryCache?.clear()
true true
} }

View file

@ -22,6 +22,7 @@ import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.BasePreferenceFragment
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
/** /**
@ -32,6 +33,7 @@ import org.oxycblt.auxio.util.navigateSafe
class PersonalizePreferenceFragment : BasePreferenceFragment(R.xml.preferences_personalize) { class PersonalizePreferenceFragment : BasePreferenceFragment(R.xml.preferences_personalize) {
override fun onOpenDialogPreference(preference: WrappedDialogPreference) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) {
if (preference.key == getString(R.string.set_key_home_tabs)) { if (preference.key == getString(R.string.set_key_home_tabs)) {
logD("Navigating to home tab dialog")
findNavController() findNavController()
.navigateSafe(PersonalizePreferenceFragmentDirections.goToTabDialog()) .navigateSafe(PersonalizePreferenceFragmentDirections.goToTabDialog())
} }

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.settings.BasePreferenceFragment
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
/** /**
@ -41,6 +42,7 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) {
override fun onOpenDialogPreference(preference: WrappedDialogPreference) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) {
if (preference.key == getString(R.string.set_key_accent)) { if (preference.key == getString(R.string.set_key_accent)) {
logD("Navigating to accent dialog")
findNavController().navigateSafe(UIPreferenceFragmentDirections.goToAccentDialog()) findNavController().navigateSafe(UIPreferenceFragmentDirections.goToAccentDialog())
} }
} }
@ -48,20 +50,25 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) {
override fun onSetupPreference(preference: Preference) { override fun onSetupPreference(preference: Preference) {
when (preference.key) { when (preference.key) {
getString(R.string.set_key_theme) -> { getString(R.string.set_key_theme) -> {
logD("Configuring theme setting")
preference.onPreferenceChangeListener = preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, value -> Preference.OnPreferenceChangeListener { _, value ->
logD("Theme changed, recreating")
AppCompatDelegate.setDefaultNightMode(value as Int) AppCompatDelegate.setDefaultNightMode(value as Int)
true true
} }
} }
getString(R.string.set_key_accent) -> { getString(R.string.set_key_accent) -> {
logD("Configuring accent setting")
preference.summary = getString(uiSettings.accent.name) preference.summary = getString(uiSettings.accent.name)
} }
getString(R.string.set_key_black_theme) -> { getString(R.string.set_key_black_theme) -> {
logD("Configuring black theme setting")
preference.onPreferenceChangeListener = preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ -> Preference.OnPreferenceChangeListener { _, _ ->
val activity = requireActivity() val activity = requireActivity()
if (activity.isNight) { if (activity.isNight) {
logD("Black theme changed in night mode, recreating")
activity.recreate() activity.recreate()
} }

View file

@ -28,6 +28,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import com.google.android.material.bottomsheet.BackportBottomSheetBehavior
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.systemGestureInsetsCompat import org.oxycblt.auxio.util.systemGestureInsetsCompat
/** /**
@ -82,6 +83,7 @@ abstract class BaseBottomSheetBehavior<V : View>(context: Context, attributeSet:
val layout = super.onLayoutChild(parent, child, layoutDirection) val layout = super.onLayoutChild(parent, child, layoutDirection)
// Don't repeat redundant initialization. // Don't repeat redundant initialization.
if (!initalized) { if (!initalized) {
logD("Not initialized, setting up child")
child.apply { child.apply {
// Set up compat elevation attributes. These are only shown below API 28. // Set up compat elevation attributes. These are only shown below API 28.
translationZ = context.getDimen(R.dimen.elevation_normal) translationZ = context.getDimen(R.dimen.elevation_normal)

View file

@ -26,6 +26,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import com.google.android.material.bottomsheet.BackportBottomSheetBehavior
import kotlin.math.abs import kotlin.math.abs
import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.coordinatorLayoutBehavior
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -60,10 +61,12 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
val behavior = dependency.coordinatorLayoutBehavior as BackportBottomSheetBehavior val behavior = dependency.coordinatorLayoutBehavior as BackportBottomSheetBehavior
val consumed = behavior.calculateConsumedByBar() val consumed = behavior.calculateConsumedByBar()
if (consumed == Int.MIN_VALUE) { if (consumed == Int.MIN_VALUE) {
logD("Not laid out yet, cannot update dependent view")
return false return false
} }
if (consumed != lastConsumed) { if (consumed != lastConsumed) {
logD("Consumed amount changed, re-applying insets")
lastConsumed = consumed lastConsumed = consumed
val insets = lastInsets val insets = lastInsets

View file

@ -30,6 +30,7 @@ import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.coordinatorLayoutBehavior
import org.oxycblt.auxio.util.logD
/** /**
* An [AppBarLayout] that resolves two issues with the default implementation: * An [AppBarLayout] that resolves two issues with the default implementation:
@ -75,6 +76,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
fun expandWithScrollingRecycler() { fun expandWithScrollingRecycler() {
setExpanded(true) setExpanded(true)
(findScrollingChild() as? RecyclerView)?.let { (findScrollingChild() as? RecyclerView)?.let {
logD("Found RecyclerView, expanding with it")
addOnOffsetChangedListener(ExpansionHackListener(it)) addOnOffsetChangedListener(ExpansionHackListener(it))
} }
} }

View file

@ -51,6 +51,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
fun setVisible(@IdRes viewId: Int): Boolean { fun setVisible(@IdRes viewId: Int): Boolean {
val index = children.indexOfFirst { it.id == viewId } val index = children.indexOfFirst { it.id == viewId }
if (index == currentlyVisible) return false if (index == currentlyVisible) return false
logD("Switching toolbar visibility from $currentlyVisible -> $index")
return animateToolbarsVisibility(currentlyVisible, index).also { 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 targetFromAlpha = 0f
val targetToAlpha = 1f val targetToAlpha = 1f
val targetDuration = val targetDuration =
// Since this view starts with the lowest toolbar index,
if (from < to) { if (from < to) {
logD("Moving higher, use an entrance animation")
context.getInteger(R.integer.anim_fade_enter_duration).toLong() context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else { } else {
logD("Moving lower, use an exit animation")
context.getInteger(R.integer.anim_fade_exit_duration).toLong() context.getInteger(R.integer.anim_fade_exit_duration).toLong()
} }
logD(targetDuration)
val fromView = getChildAt(from) as Toolbar val fromView = getChildAt(from) as Toolbar
val toView = getChildAt(to) as Toolbar val toView = getChildAt(to) as Toolbar
@ -80,15 +82,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (!isLaidOut) { if (!isLaidOut) {
// Not laid out, just change it immediately while are not shown to the user. // Not laid out, just change it immediately while are not shown to the user.
// This is an initialization, so we return false despite changing. // This is an initialization, so we return false despite changing.
logD("Not laid out, immediately updating visibility")
setToolbarsAlpha(fromView, toView, targetFromAlpha) setToolbarsAlpha(fromView, toView, targetFromAlpha)
return false return false
} }
if (fadeThroughAnimator != null) { logD("Changing toolbar visibility $from -> 0f, $to -> 1f")
fadeThroughAnimator?.cancel() fadeThroughAnimator?.cancel()
fadeThroughAnimator = null
}
fadeThroughAnimator = fadeThroughAnimator =
ValueAnimator.ofFloat(fromView.alpha, targetFromAlpha).apply { ValueAnimator.ofFloat(fromView.alpha, targetFromAlpha).apply {
duration = targetDuration duration = targetDuration
@ -100,7 +100,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
private fun setToolbarsAlpha(from: Toolbar, to: Toolbar, innerAlpha: Float) { private fun setToolbarsAlpha(from: Toolbar, to: Toolbar, innerAlpha: Float) {
logD("${to.id == R.id.detail_edit_toolbar} ${1 - innerAlpha}")
from.apply { from.apply {
alpha = innerAlpha alpha = innerAlpha
isInvisible = innerAlpha == 0f isInvisible = innerAlpha == 0f

Some files were not shown because too many files have changed in this diff Show more