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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.detail.header
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.util.logD
/**
* A [RecyclerView.Adapter] that implements shared behavior between each parent header view.
@ -47,6 +48,7 @@ abstract class DetailHeaderAdapter<T : MusicParent, VH : RecyclerView.ViewHolder
* @param parent The new [MusicParent] to show.
*/
fun setParent(parent: T) {
logD("Updating parent [old: $currentParent new: $parent]")
currentParent = parent
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.
*/
protected fun rebindParent() {
logD("Rebinding parent")
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.getPlural
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/**
* A [DetailHeaderAdapter] that shows [Playlist] information.
@ -57,6 +58,7 @@ class PlaylistDetailHeaderAdapter(private val listener: Listener) :
// Nothing to do.
return
}
logD("Updating editing state [old: ${editedPlaylist?.size} new: ${songs?.size}")
editedPlaylist = songs
rebindParent()
}
@ -102,12 +104,17 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
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 {
isEnabled = playlist.songs.isNotEmpty() && editedPlaylist == null
isEnabled = playable
setOnClickListener { listener.onPlay() }
}
binding.detailShuffleButton.apply {
isEnabled = playlist.songs.isNotEmpty() && editedPlaylist == null
isEnabled = playable
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.util.context
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.
@ -116,6 +117,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
isGone = text == null
}
} else {
logD("Disc is null, defaulting to no disc")
binding.discNumber.text = binding.context.getString(R.string.def_disc)
binding.discName.isGone = true
}

View file

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

View file

@ -48,6 +48,7 @@ import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/**
* A [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.
return
}
logD("Updating editing state [old: $isEditing new: $editing]")
this.isEditing = editing
notifyItemRangeChanged(1, currentList.size - 1, PAYLOAD_EDITING_CHANGED)
}

View file

@ -44,23 +44,32 @@ constructor(
override fun show() {
// 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
// a flip was canceled by a hide.
pendingConfig?.run {
this@FlipFloatingActionButton.logD("Applying pending configuration")
setImageResource(iconRes)
contentDescription = context.getString(contentDescriptionRes)
setOnClickListener(clickListener)
}
pendingConfig = null
logD("Beginning show")
super.show()
}
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.
flipping = false
// Don't pass any kind of listener so that future flip operations will not be able
// to show the FAB again.
logD("Beginning hide")
super.hide()
}
@ -82,9 +91,12 @@ constructor(
// Already hiding for whatever reason, apply the configuration when the FAB is shown again.
if (!isOrWillBeHidden) {
logD("Starting hide for flip")
flipping = true
// We will re-show the FAB later, assuming that there was not a prior flip operation.
super.hide(FlipVisibilityListener())
} else {
logD("Already hiding, will apply config later")
}
}
@ -97,7 +109,7 @@ constructor(
private inner class FlipVisibilityListener : OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton) {
if (!flipping) return
logD("Showing for a flip operation")
logD("Starting show for flip")
flipping = false
show()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -43,7 +43,6 @@ import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
/**
* A [ListFragment] that shows a list of [Playlist]s.
@ -120,8 +119,7 @@ class PlaylistListFragment :
}
private fun updatePlaylists(playlists: List<Playlist>) {
playlistAdapter.update(
playlists, homeModel.playlistsInstructions.consume().also { logD(it) })
playlistAdapter.update(playlists, homeModel.playlistsInstructions.consume())
}
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 org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.logD
/**
* A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations
@ -67,20 +66,11 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List<MusicMode>) :
// Use expected sw* size thresholds when choosing a configuration.
when {
// On small screens, only display an icon.
width < 370 -> {
logD("Using icon-only configuration")
tab.setIcon(icon).setContentDescription(string)
}
width < 370 -> tab.setIcon(icon).setContentDescription(string)
// On large screens, display an icon and text.
width < 600 -> {
logD("Using text-only configuration")
tab.setText(string)
}
width < 600 -> tab.setText(string)
// On medium-size screens, display text.
else -> {
logD("Using icon-and-text configuration")
tab.setIcon(icon).setText(string)
}
else -> tab.setIcon(icon).setText(string)
}
}
}

View file

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

View file

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

View file

@ -91,14 +91,15 @@ class TabCustomizeDialog :
// We will need the exact index of the tab to update on in order to
// notify the adapter of the change.
val index = tabAdapter.tabs.indexOfFirst { it.mode == item.mode }
val tab = tabAdapter.tabs[index]
tabAdapter.setTab(
index,
when (tab) {
val old = tabAdapter.tabs[index]
val new =
when (old) {
// Invert the visibility of the tab
is Tab.Visible -> Tab.Invisible(tab.mode)
is Tab.Invisible -> Tab.Visible(tab.mode)
})
is Tab.Visible -> Tab.Invisible(old.mode)
is Tab.Invisible -> Tab.Visible(old.mode)
}
logD("Flipping tab visibility [from: $old to: $new]")
tabAdapter.setTab(index, new)
// Prevent the user from saving if all the tabs are Invisible, as that's an invalid state.
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =

View file

@ -63,7 +63,9 @@ class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callbac
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
throw IllegalStateException()
}
// We use a custom drag handle, so disable the long press action.
override fun isLongPressDragEnabled() = false

View file

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

View file

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

View file

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

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.list
import androidx.annotation.StringRes
// TODO: Consider breaking this up into sealed classes for individual adapters
/** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
interface Item

View file

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

View file

@ -22,8 +22,6 @@ import androidx.annotation.IdRes
import kotlin.math.max
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Sort.Direction
import org.oxycblt.auxio.list.Sort.Mode
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre

View file

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

View file

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

View file

@ -22,6 +22,7 @@ import android.view.View
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.util.logD
/**
* A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of
@ -54,6 +55,7 @@ abstract class SelectionIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
// Nothing to do.
return
}
logD("Updating selection [old=${oldSelectedItems.size} new=${newSelectedItems.size}")
selectedItems = newSelectedItems
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.
// TODO: I think this is possible to improve with a raw ValueAnimator.
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
logD("Lifting item")
logD("Lifting ViewHolder")
val bg = holder.background
val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal)
@ -110,7 +110,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() {
// This function can be called multiple times, so only start the animation when the view's
// translationZ is already non-zero.
if (holder.root.translationZ != 0f) {
logD("Dropping item")
logD("Lifting ViewHolder")
val bg = holder.background
val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal)
@ -137,7 +137,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() {
// Long-press events are too buggy, only allow dragging with the handle.
final override fun isLongPressDragEnabled() = false
/** Required [RecyclerView.ViewHolder] implementation that exposes the following. */
/** Required [RecyclerView.ViewHolder] implementation that exposes required fields */
interface ViewHolder {
/** Whether this [ViewHolder] can be moved right now. */
val enabled: Boolean

View file

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

View file

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

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.fs.Directory
import org.oxycblt.auxio.music.fs.MusicDirectories
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD
/**
* User configuration specific to music system.
@ -231,8 +232,14 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
getString(R.string.set_key_music_dirs),
getString(R.string.set_key_music_dirs_include),
getString(R.string.set_key_separators),
getString(R.string.set_key_auto_sort_names) -> listener.onIndexingSettingChanged()
getString(R.string.set_key_observing) -> listener.onObservingChanged()
getString(R.string.set_key_auto_sort_names) -> {
logD("Dispatching indexing setting change for $key")
listener.onIndexingSettingChanged()
}
getString(R.string.set_key_observing) -> {
logD("Dispatching observing setting change")
listener.onObservingChanged()
}
}
}
}

View file

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

View file

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

View file

@ -149,6 +149,9 @@ private class DeviceLibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings
return hashCode
}
override fun toString() =
"DeviceLibrary(songs=${songs.size}, albums=${albums.size}, artists=${artists.size}, genres=${genres.size})"
override fun findSong(uid: Music.UID) = songUidMap[uid]
override fun findAlbum(uid: Music.UID) = albumUidMap[uid]
override fun findArtist(uid: Music.UID) = artistUidMap[uid]

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 equals(other: Any?) =
other is SongImpl && uid == other.uid && rawSong == other.rawSong
override fun toString() = "Song(uid=$uid, name=$name)"
private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings)
private val artistNames = rawSong.artistNames.parseMultiValue(musicSettings)
@ -262,6 +263,8 @@ class AlbumImpl(
override fun equals(other: Any?) =
other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs
override fun toString() = "Album(uid=$uid, name=$name)"
private val _artists = mutableListOf<ArtistImpl>()
override val artists: List<Artist>
get() = _artists
@ -363,6 +366,8 @@ class ArtistImpl(
rawArtist == other.rawArtist &&
songs == other.songs
override fun toString() = "Artist(uid=$uid, name=$name)"
override lateinit var genres: List<Genre>
init {
@ -449,6 +454,8 @@ class GenreImpl(
override fun equals(other: Any?) =
other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs
override fun toString() = "Genre(uid=$uid, name=$name)"
init {
val distinctAlbums = mutableSetOf<Album>()
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.util.context
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/**
* [RecyclerView.Adapter] that manages a list of [Directory] instances.
@ -54,10 +55,8 @@ class DirectoryAdapter(private val listener: Listener) :
* @param dir The [Directory] to add.
*/
fun add(dir: Directory) {
if (_dirs.contains(dir)) {
return
}
if (_dirs.contains(dir)) return
logD("Adding $dir")
_dirs.add(dir)
notifyItemInserted(_dirs.lastIndex)
}
@ -65,9 +64,10 @@ class DirectoryAdapter(private val listener: Listener) :
/**
* Add a list of [Directory] instances to the end of the list.
*
* @param dirs The [Directory instances to add.
* @param dirs The [Directory] instances to add.
*/
fun addAll(dirs: List<Directory>) {
logD("Adding ${dirs.size} directories")
val oldLastIndex = dirs.lastIndex
_dirs.addAll(dirs)
notifyItemRangeInserted(oldLastIndex, dirs.size)
@ -79,6 +79,7 @@ class DirectoryAdapter(private val listener: Listener) :
* @param dir The [Directory] to remove. Must exist in the list.
*/
fun remove(dir: Directory) {
logD("Removing $dir")
val idx = _dirs.indexOf(dir)
_dirs.removeAt(idx)
notifyItemRemoved(idx)
@ -86,6 +87,7 @@ class DirectoryAdapter(private val listener: Listener) :
/** A Listener for [DirectoryAdapter] interactions. */
interface Listener {
/** Called when the delete button on a directory item is clicked. */
fun onRemoveDirectory(dir: Directory)
}
}

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
* obtained.
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: Get around to simplifying this
*/
data class MimeType(val fromExtension: String, val fromFormat: String?) {
/**
* Return a mime-type such as "audio/ogg"
*
* @return A raw mime-type string. Will first try [fromFormat], then falling back to
* [fromExtension], and then null if that fails.
*/
val raw: String
get() = fromFormat ?: fromExtension
/**
* Resolve the mime type into a human-readable format name, such as "Ogg Vorbis".
*

View file

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

View file

@ -24,6 +24,7 @@ import java.text.SimpleDateFormat
import kotlin.math.max
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.nonZeroOrNull
/**
@ -51,27 +52,25 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
* 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
* be properly localized.
*/
fun resolveDate(context: Context): String {
if (month != null) {
// Parse a date format from an ISO-ish format
val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
format.applyPattern("yyyy-MM")
val date =
try {
format.parse("$year-$month")
} catch (e: ParseException) {
null
}
if (date != null) {
// Reformat as a readable month and year
format.applyPattern("MMM yyyy")
return format.format(date)
}
}
fun resolve(context: Context) =
// Unable to create fine-grained date, just format as a year.
return context.getString(R.string.fmt_number, year)
month?.let { resolveFineGrained() } ?: context.getString(R.string.fmt_number, year)
private fun resolveFineGrained(): String? {
// We can't directly load a date with our own
val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
format.applyPattern("yyyy-MM")
val date =
try {
format.parse("$year-$month")
} catch (e: ParseException) {
logE("Unable to parse fine-grained date: $e")
return null
}
// Reformat as a readable month and year
format.applyPattern("MMM yyyy")
return format.format(date)
}
override fun hashCode() = tokens.hashCode()
@ -139,9 +138,9 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
fun resolveDate(context: Context) =
if (min != max) {
context.getString(
R.string.fmt_date_range, min.resolveDate(context), max.resolveDate(context))
R.string.fmt_date_range, min.resolve(context), max.resolve(context))
} else {
min.resolveDate(context)
min.resolve(context)
}
override fun equals(other: Any?) = other is Range && min == other.min && max == other.max

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.
*/
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 hashCode() = number.hashCode()
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.
if (token.first().isDigit()) {
// The digit string comparison breaks with preceding zero digits, remove those
// TODO: Handle zero digits in other languages
val digits = token.trimStart('0').ifEmpty { token }
// Other languages have other types of digit strings, still use collation keys
collationKey = COLLATOR.getCollationKey(digits)

View file

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

View file

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

View file

@ -23,6 +23,7 @@ import javax.inject.Inject
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.util.logD
/**
* The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the
@ -52,6 +53,8 @@ class TagExtractorImpl @Inject constructor(private val tagWorkerFactory: TagWork
// producing similar throughput's to other kinds of manual metadata extraction.
val tagWorkerPool: Array<TagWorker?> = arrayOfNulls(TASK_CAPACITY)
logD("Beginning primary extraction loop")
for (incompleteRawSong in incompleteSongs) {
spin@ while (true) {
for (i in tagWorkerPool.indices) {
@ -71,6 +74,8 @@ class TagExtractorImpl @Inject constructor(private val tagWorkerFactory: TagWork
}
}
logD("All incomplete songs exhausted, starting cleanup loop")
do {
var ongoingTasks = false
for (i in tagWorkerPool.indices) {

View file

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

View file

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

View file

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

View file

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

View file

@ -31,6 +31,9 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW
/**
* A [ViewModel] managing the state of the playlist picker dialogs.
@ -84,6 +87,8 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
pendingPlaylist.preferredName,
pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) })
}
logD("Updated pending playlist: ${_currentPendingPlaylist.value?.preferredName}")
_currentSongsToAdd.value =
_currentSongsToAdd.value?.let { pendingSongs ->
pendingSongs
@ -91,6 +96,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
.ifEmpty { null }
.also { refreshChoicesWith = it }
}
logD("Updated songs to add: ${_currentSongsToAdd.value?.size} songs")
}
val chosenName = _chosenName.value
@ -102,6 +108,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
// Nothing to do.
}
}
logD("Updated chosen name to $chosenName")
refreshChoicesWith = refreshChoicesWith ?: _currentSongsToAdd.value
}
@ -119,19 +126,34 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
* @param songUids The [Music.UID]s of songs to be present in the playlist.
*/
fun setPendingPlaylist(context: Context, songUids: Array<Music.UID>) {
val deviceLibrary = musicRepository.deviceLibrary ?: return
val songs = songUids.mapNotNull(deviceLibrary::findSong)
logD("Opening ${songUids.size} songs to create a playlist from")
val userLibrary = musicRepository.userLibrary ?: return
var i = 1
while (true) {
val possibleName = context.getString(R.string.fmt_def_playlist, i)
if (userLibrary.playlists.none { it.name.resolve(context) == possibleName }) {
_currentPendingPlaylist.value = PendingPlaylist(possibleName, songs)
return
val songs =
musicRepository.deviceLibrary
?.let { songUids.mapNotNull(it::findSong) }
?.also(::refreshPlaylistChoices)
val possibleName =
musicRepository.userLibrary?.let {
// Attempt to generate a unique default name for the playlist, like "Playlist 1".
var i = 1
var possibleName: String
do {
possibleName = context.getString(R.string.fmt_def_playlist, i)
logD("Trying $possibleName as a playlist name")
++i
} while (userLibrary.playlists.any { it.name.resolve(context) == possibleName })
logD("$possibleName is unique, using it as the playlist name")
possibleName
}
_currentPendingPlaylist.value =
if (possibleName != null && songs != null) {
PendingPlaylist(possibleName, songs)
} else {
logW("Given song UIDs to create were invalid")
null
}
++i
}
}
/**
@ -140,7 +162,11 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
* @param playlistUid The [Music.UID]s of the [Playlist] to rename.
*/
fun setPlaylistToRename(playlistUid: Music.UID) {
logD("Opening playlist $playlistUid to rename")
_currentPlaylistToRename.value = musicRepository.userLibrary?.findPlaylist(playlistUid)
if (_currentPlaylistToDelete.value == null) {
logW("Given playlist UID to rename was invalid")
}
}
/**
@ -149,7 +175,11 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
* @param playlistUid The [Music.UID] of the [Playlist] to delete.
*/
fun setPlaylistToDelete(playlistUid: Music.UID) {
logD("Opening playlist $playlistUid to delete")
_currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid)
if (_currentPlaylistToDelete.value == null) {
logW("Given playlist UID to delete was invalid")
}
}
/**
@ -158,16 +188,25 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
* @param name The new user-inputted name, or null if not present.
*/
fun updateChosenName(name: String?) {
logD("Updating chosen name to $name")
_chosenName.value =
when {
name.isNullOrEmpty() -> ChosenName.Empty
name.isBlank() -> ChosenName.Blank
name.isNullOrEmpty() -> {
logE("Chosen name is empty")
ChosenName.Empty
}
name.isBlank() -> {
logE("Chosen name is blank")
ChosenName.Blank
}
else -> {
val trimmed = name.trim()
val userLibrary = musicRepository.userLibrary
if (userLibrary != null && userLibrary.findPlaylist(trimmed) == null) {
logD("Chosen name is valid")
ChosenName.Valid(trimmed)
} else {
logD("Chosen name already exists in library")
ChosenName.AlreadyExists(trimmed)
}
}
@ -180,14 +219,19 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
* @param songUids The [Music.UID]s of songs to add to a playlist.
*/
fun setSongsToAdd(songUids: Array<Music.UID>) {
val deviceLibrary = musicRepository.deviceLibrary ?: return
val songs = songUids.mapNotNull(deviceLibrary::findSong)
_currentSongsToAdd.value = songs
refreshPlaylistChoices(songs)
logD("Opening ${songUids.size} songs to add to a playlist")
_currentSongsToAdd.value =
musicRepository.deviceLibrary
?.let { songUids.mapNotNull(it::findSong).ifEmpty { null } }
?.also(::refreshPlaylistChoices)
if (_currentSongsToAdd.value == null || songUids.size != _currentSongsToAdd.value?.size) {
logW("Given song UIDs to add were (partially) invalid")
}
}
private fun refreshPlaylistChoices(songs: List<Song>) {
val userLibrary = musicRepository.userLibrary ?: return
logD("Refreshing playlist choices")
_playlistAddChoices.value =
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map {
val songSet = it.songs.toSet()

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
@ -86,7 +87,9 @@ class RenamePlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding
}
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
}
}

View file

@ -124,6 +124,7 @@ class IndexerService :
// --- CONTROLLER CALLBACKS ---
override fun requestIndex(withCache: Boolean) {
logD("Starting new indexing job")
// Cancel the previous music loading job.
currentIndexJob?.cancel()
// Start a new music loading job on a co-routine.
@ -137,6 +138,7 @@ class IndexerService :
override fun onMusicChanges(changes: MusicRepository.Changes) {
val deviceLibrary = musicRepository.deviceLibrary ?: return
logD("Music changed, updating shared objects")
// Wipe possibly-invalidated outdated covers
imageLoader.memoryCache?.clear()
// 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.
// 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.
logD("Need to observe, staying in foreground")
if (!foregroundManager.tryStartForeground(observingNotification)) {
logD("Notification changed, re-posting notification")
observingNotification.post()
}
} else {
// Not observing and done loading, exit foreground.
logD("Exiting foreground")
foregroundManager.tryStopForeground()
}
// 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
// the music loading process ends.
if (currentIndexJob == null) {
logD("Not loading, updating idle session")
updateIdleSession()
}
}
@ -274,6 +280,7 @@ class IndexerService :
// 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.
if (musicSettings.shouldBeObserving) {
logD("MediaStore changed, starting re-index")
requestIndex(true)
}
}

View file

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

View file

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

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.util.logD
*
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: This whole system is very jankily designed, perhaps it's time for a refactor?
* TODO: Unwind this into ViewModel-specific actions, and then reference those.
*/
class NavigationViewModel : ViewModel() {
private val _mainNavigationAction = MutableEvent<MainNavigationAction>()
@ -96,6 +96,7 @@ class NavigationViewModel : ViewModel() {
* dialog will be shown.
*/
fun exploreNavigateToParentArtist(song: Song) {
logD("Navigating to parent artist of $song")
exploreNavigateToParentArtistImpl(song, song.artists)
}
@ -106,6 +107,7 @@ class NavigationViewModel : ViewModel() {
* dialog will be shown.
*/
fun exploreNavigateToParentArtist(album: Album) {
logD("Navigating to parent artist of $album")
exploreNavigateToParentArtistImpl(album, album.artists)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,6 +27,8 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/**
* A [ViewModel] that stores the choices shown in the playback picker dialogs.
@ -62,6 +64,10 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M
* @param uid The [Music.UID] of the item to show. Must be a [Song].
*/
fun setPickerSongUid(uid: Music.UID) {
logD("Opening picker for song $uid")
_currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid)
if (_currentPickerSong.value != null) {
logW("Given song UID was invalid")
}
}
}

View file

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

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
// as playing.
// TODO: Optimize this by only updating the range between old and new indices?
// TODO: Don't update when the index has not moved.
if (currentIndex < lastIndex) {
logD("Moved backwards, must update items above last index")
notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_POSITION)
} else {
logD("Moved forwards, update items after index")
notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_POSITION)
}
@ -121,6 +125,10 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS
alpha = 0
}
/**
* Whether this ViewHolder should be full-opacity to represent a future item, or greyed out to
* represent a past item. True if former, false if latter.
*/
var isFuture: Boolean
get() = binding.songAlbumCover.isEnabled
set(value) {

View file

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

View file

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

View file

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

View file

@ -125,14 +125,22 @@ constructor(
when (playbackSettings.replayGainMode) {
// User wants track gain to be preferred. Default to album gain only if
// there is no track gain.
ReplayGainMode.TRACK -> gain.track == 0f
ReplayGainMode.TRACK -> {
logD("Using track strategy")
gain.track == 0f
}
// User wants album gain to be preferred. Default to track gain only if
// here is no album gain.
ReplayGainMode.ALBUM -> gain.album != 0f
ReplayGainMode.ALBUM -> {
logD("Using album strategy")
gain.album != 0f
}
// User wants album gain to be used when in an album, track gain otherwise.
ReplayGainMode.DYNAMIC ->
ReplayGainMode.DYNAMIC -> {
logD("Using dynamic strategy")
playbackManager.parent is Album &&
playbackManager.queue.currentSong?.album == playbackManager.parent
}
}
val resolvedGain =
@ -184,6 +192,7 @@ constructor(
textTags.vorbis[TAG_RG_TRACK_GAIN]
?.run { first().parseReplayGainAdjustment() }
?.let { albumGain = it }
// Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the
// adjustment by 256 to get the gain. This is used alongside the base adjustment
// intrinsic to the format to create the normalized adjustment. This is normally the only

View file

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

View file

@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD
/**
* A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to [PlaybackService].
@ -43,6 +44,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
// stupid this is with the state of foreground services on modern android. One
// wrong action at the wrong time will result in the app crashing, and there is
// nothing I can do about it.
logD("Delivering media button intent $intent")
intent.component = ComponentName(context, PlaybackService::class.java)
ContextCompat.startForegroundService(context, intent)
}

View file

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

View file

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

View file

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

View file

@ -25,6 +25,7 @@ import com.google.android.material.button.MaterialButton
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.RippleFixMaterialButton
import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.logD
/**
* A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when
@ -46,10 +47,12 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
val targetRadius = if (activated) 0.3f else 0.5f
if (!isLaidOut) {
// Not laid out, initialize it without animation before drawing.
logD("Not laid out, immediately updating corner radius")
updateCornerRadiusRatio(targetRadius)
return
}
logD("Starting corner radius animation")
animator?.cancel()
animator =
ValueAnimator.ofFloat(currentCornerRadiusRatio, targetRadius).apply {

View file

@ -81,6 +81,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
// zero, use 1 instead and disable the SeekBar.
val to = max(value, 1)
isEnabled = value > 0
logD("Value sanitization finished [to=$to, enabled=$isEnabled]")
// Sanity check 2: If the current value exceeds the new duration value, clamp it
// down so that we don't crash and instead have an annoying visual flicker.
if (positionDs > to) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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