all: remove superfluous comments

Remove superflous comments that really add nothing.
This commit is contained in:
Alexander Capehart 2022-12-23 11:07:32 -07:00
parent 7415c28e2d
commit b38b8a909f
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
57 changed files with 481 additions and 960 deletions

View file

@ -27,7 +27,7 @@ import coil.ImageLoaderFactory
import coil.request.CachePolicy import coil.request.CachePolicy
import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher
import org.oxycblt.auxio.image.extractor.ArtistImageFetcher import org.oxycblt.auxio.image.extractor.ArtistImageFetcher
import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFractory import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory
import org.oxycblt.auxio.image.extractor.GenreImageFetcher import org.oxycblt.auxio.image.extractor.GenreImageFetcher
import org.oxycblt.auxio.image.extractor.MusicKeyer import org.oxycblt.auxio.image.extractor.MusicKeyer
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
@ -68,7 +68,7 @@ class AuxioApp : Application(), ImageLoaderFactory {
add(GenreImageFetcher.Factory()) add(GenreImageFetcher.Factory())
} }
// Use our own crossfade with error drawable support // Use our own crossfade with error drawable support
.transitionFactory(ErrorCrossfadeTransitionFractory()) .transitionFactory(ErrorCrossfadeTransitionFactory())
// Not downloading anything, so no disk-caching // Not downloading anything, so no disk-caching
.diskCachePolicy(CachePolicy.DISABLED) .diskCachePolicy(CachePolicy.DISABLED)
.build() .build()

View file

@ -17,144 +17,102 @@
package org.oxycblt.auxio package org.oxycblt.auxio
/** A table containing all unique integer codes that Auxio uses. */ /**
* A table containing all of the magic integer codes that the codebase has currently reserved.
* May be non-contiguous.
* @author Alexander Capehart (OxygenCobalt)
*/
object IntegerTable { object IntegerTable {
/** SongViewHolder */ /** SongViewHolder */
const val VIEW_TYPE_SONG = 0xA000 const val VIEW_TYPE_SONG = 0xA000
/** AlbumViewHolder */ /** AlbumViewHolder */
const val VIEW_TYPE_ALBUM = 0xA001 const val VIEW_TYPE_ALBUM = 0xA001
/** ArtistViewHolder */ /** ArtistViewHolder */
const val VIEW_TYPE_ARTIST = 0xA002 const val VIEW_TYPE_ARTIST = 0xA002
/** GenreViewHolder */ /** GenreViewHolder */
const val VIEW_TYPE_GENRE = 0xA003 const val VIEW_TYPE_GENRE = 0xA003
/** HeaderViewHolder */ /** HeaderViewHolder */
const val VIEW_TYPE_HEADER = 0xA004 const val VIEW_TYPE_HEADER = 0xA004
/** SortHeaderViewHolder */ /** SortHeaderViewHolder */
const val VIEW_TYPE_SORT_HEADER = 0xA005 const val VIEW_TYPE_SORT_HEADER = 0xA005
/** AlbumDetailViewHolder */ /** AlbumDetailViewHolder */
const val VIEW_TYPE_ALBUM_DETAIL = 0xA006 const val VIEW_TYPE_ALBUM_DETAIL = 0xA006
/** AlbumSongViewHolder */ /** AlbumSongViewHolder */
const val VIEW_TYPE_ALBUM_SONG = 0xA007 const val VIEW_TYPE_ALBUM_SONG = 0xA007
/** ArtistDetailViewHolder */ /** ArtistDetailViewHolder */
const val VIEW_TYPE_ARTIST_DETAIL = 0xA008 const val VIEW_TYPE_ARTIST_DETAIL = 0xA008
/** ArtistAlbumViewHolder */ /** ArtistAlbumViewHolder */
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009 const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
/** ArtistSongViewHolder */ /** ArtistSongViewHolder */
const val VIEW_TYPE_ARTIST_SONG = 0xA00A const val VIEW_TYPE_ARTIST_SONG = 0xA00A
/** GenreDetailViewHolder */ /** GenreDetailViewHolder */
const val VIEW_TYPE_GENRE_DETAIL = 0xA00B const val VIEW_TYPE_GENRE_DETAIL = 0xA00B
/** DiscHeaderViewHolder */ /** DiscHeaderViewHolder */
const val VIEW_TYPE_DISC_HEADER = 0xA00C const val VIEW_TYPE_DISC_HEADER = 0xA00C
/** "Music playback" notification code */ /** "Music playback" notification code */
const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0 const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0
/** "Music loading" notification code */ /** "Music loading" notification code */
const val INDEXER_NOTIFICATION_CODE = 0xA0A1 const val INDEXER_NOTIFICATION_CODE = 0xA0A1
/** MainActivity Intent request code */
/** Intent request code */
const val REQUEST_CODE = 0xA0C0 const val REQUEST_CODE = 0xA0C0
/** RepeatMode.NONE */ /** RepeatMode.NONE */
const val REPEAT_MODE_NONE = 0xA100 const val REPEAT_MODE_NONE = 0xA100
/** RepeatMode.ALL */ /** RepeatMode.ALL */
const val REPEAT_MODE_ALL = 0xA101 const val REPEAT_MODE_ALL = 0xA101
/** RepeatMode.TRACK */ /** RepeatMode.TRACK */
const val REPEAT_MODE_TRACK = 0xA102 const val REPEAT_MODE_TRACK = 0xA102
/** PlaybackMode.IN_ARTIST */ /** PlaybackMode.IN_ARTIST */
const val PLAYBACK_MODE_IN_ARTIST = 0xA104 const val PLAYBACK_MODE_IN_ARTIST = 0xA104
/** PlaybackMode.IN_ALBUM */ /** PlaybackMode.IN_ALBUM */
const val PLAYBACK_MODE_IN_ALBUM = 0xA105 const val PLAYBACK_MODE_IN_ALBUM = 0xA105
/** PlaybackMode.ALL_SONGS */ /** PlaybackMode.ALL_SONGS */
const val PLAYBACK_MODE_ALL_SONGS = 0xA106 const val PLAYBACK_MODE_ALL_SONGS = 0xA106
/** DisplayMode.NONE (No Longer used but still reserved) */ /** DisplayMode.NONE (No Longer used but still reserved) */
// const val DISPLAY_MODE_NONE = 0xA107 // const val DISPLAY_MODE_NONE = 0xA107
/** MusicMode._GENRES */ /** MusicMode._GENRES */
const val MUSIC_MODE_GENRES = 0xA108 const val MUSIC_MODE_GENRES = 0xA108
/** MusicMode._ARTISTS */ /** MusicMode._ARTISTS */
const val MUSIC_MODE_ARTISTS = 0xA109 const val MUSIC_MODE_ARTISTS = 0xA109
/** MusicMode._ALBUMS */ /** MusicMode._ALBUMS */
const val MUSIC_MODE_ALBUMS = 0xA10A const val MUSIC_MODE_ALBUMS = 0xA10A
/** MusicMode.SONGS */
/** MusicMode._SONGS */
const val MUSIC_MODE_SONGS = 0xA10B const val MUSIC_MODE_SONGS = 0xA10B
// Note: Sort integer codes are non-contiguous due to significant amounts of time
// passing between the additions of new sort modes.
/** Sort.ByName */ /** Sort.ByName */
const val SORT_BY_NAME = 0xA10C const val SORT_BY_NAME = 0xA10C
/** Sort.ByArtist */ /** Sort.ByArtist */
const val SORT_BY_ARTIST = 0xA10D const val SORT_BY_ARTIST = 0xA10D
/** Sort.ByAlbum */ /** Sort.ByAlbum */
const val SORT_BY_ALBUM = 0xA10E const val SORT_BY_ALBUM = 0xA10E
/** Sort.ByYear */ /** Sort.ByYear */
const val SORT_BY_YEAR = 0xA10F const val SORT_BY_YEAR = 0xA10F
/** Sort.ByDuration */ /** Sort.ByDuration */
const val SORT_BY_DURATION = 0xA114 const val SORT_BY_DURATION = 0xA114
/** Sort.ByCount */ /** Sort.ByCount */
const val SORT_BY_COUNT = 0xA115 const val SORT_BY_COUNT = 0xA115
/** Sort.ByDisc */ /** Sort.ByDisc */
const val SORT_BY_DISC = 0xA116 const val SORT_BY_DISC = 0xA116
/** Sort.ByTrack */ /** Sort.ByTrack */
const val SORT_BY_TRACK = 0xA117 const val SORT_BY_TRACK = 0xA117
/** Sort.ByDateAdded */ /** Sort.ByDateAdded */
const val SORT_BY_DATE_ADDED = 0xA118 const val SORT_BY_DATE_ADDED = 0xA118
/** ReplayGainMode.Off (No longer used but still reserved) */ /** ReplayGainMode.Off (No longer used but still reserved) */
// const val REPLAY_GAIN_MODE_OFF = 0xA110 // const val REPLAY_GAIN_MODE_OFF = 0xA110
/** ReplayGainMode.Track */ /** ReplayGainMode.Track */
const val REPLAY_GAIN_MODE_TRACK = 0xA111 const val REPLAY_GAIN_MODE_TRACK = 0xA111
/** ReplayGainMode.Album */ /** ReplayGainMode.Album */
const val REPLAY_GAIN_MODE_ALBUM = 0xA112 const val REPLAY_GAIN_MODE_ALBUM = 0xA112
/** ReplayGainMode.Dynamic */ /** ReplayGainMode.Dynamic */
const val REPLAY_GAIN_MODE_DYNAMIC = 0xA113 const val REPLAY_GAIN_MODE_DYNAMIC = 0xA113
/** ActionMode.Next */ /** ActionMode.Next */
const val ACTION_MODE_NEXT = 0xA119 const val ACTION_MODE_NEXT = 0xA119
/** ActionMode.Repeat */ /** ActionMode.Repeat */
const val ACTION_MODE_REPEAT = 0xA11A const val ACTION_MODE_REPEAT = 0xA11A
/** ActionMode.Shuffle */ /** ActionMode.Shuffle */
const val ACTION_MODE_SHUFFLE = 0xA11B const val ACTION_MODE_SHUFFLE = 0xA11B
/** CoverMode.Off */ /** CoverMode.Off */
const val COVER_MODE_OFF = 0xA11C const val COVER_MODE_OFF = 0xA11C
/** CoverMode.MediaStore */ /** CoverMode.MediaStore */
const val COVER_MODE_MEDIA_STORE = 0xA11D const val COVER_MODE_MEDIA_STORE = 0xA11D
/** CoverMode.Quality */ /** CoverMode.Quality */
const val COVER_MODE_QUALITY = 0xA11E const val COVER_MODE_QUALITY = 0xA11E
} }

View file

@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
* The single [AppCompatActivity] for Auxio. * Auxio's single [AppCompatActivity].
* *
* TODO: Add error screens * TODO: Add error screens
* *
@ -85,6 +85,14 @@ class MainActivity : AppCompatActivity() {
startIntentAction(intent) startIntentAction(intent)
} }
/**
* Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action]
* that can be used in the playback system.
* @param intent The (new) [Intent] given to this [MainActivity], or null if there
* is no intent.
* @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started,
* false otherwise.
*/
private fun startIntentAction(intent: Intent?): Boolean { private fun startIntentAction(intent: Intent?): Boolean {
if (intent == null) { if (intent == null) {
return false return false
@ -97,7 +105,6 @@ class MainActivity : AppCompatActivity() {
// RestoreState action. // RestoreState action.
return true return true
} }
intent.putExtra(KEY_INTENT_USED, true) intent.putExtra(KEY_INTENT_USED, true)
val action = val action =
@ -106,19 +113,16 @@ class MainActivity : AppCompatActivity() {
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
else -> return false else -> return false
} }
playbackModel.startAction(action) playbackModel.startAction(action)
return true return true
} }
private fun setupTheme() { private fun setupTheme() {
val settings = Settings(this) val settings = Settings(this)
// Set up the current theme.
AppCompatDelegate.setDefaultNightMode(settings.theme) AppCompatDelegate.setDefaultNightMode(settings.theme)
// Set up the color scheme. Note that the black theme has it's own set
// The black theme has a completely separate set of styles since style attributes cannot // of styles since the color schemes cannot be modified at runtime.
// be modified at runtime.
if (isNight && settings.useBlackTheme) { if (isNight && settings.useBlackTheme) {
logD("Applying black theme [accent ${settings.accent}]") logD("Applying black theme [accent ${settings.accent}]")
setTheme(settings.accent.blackTheme) setTheme(settings.accent.blackTheme)
@ -130,7 +134,6 @@ class MainActivity : AppCompatActivity() {
private fun setupEdgeToEdge(contentView: View) { private fun setupEdgeToEdge(contentView: View) {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
contentView.setOnApplyWindowInsetsListener { view, insets -> contentView.setOnApplyWindowInsetsListener { view, insets ->
val bars = insets.systemBarInsetsCompat val bars = insets.systemBarInsetsCompat
view.updatePadding(left = bars.left, right = bars.right) view.updatePadding(left = bars.left, right = bars.right)

View file

@ -121,7 +121,7 @@ class MainFragment :
collect(navModel.exploreNavigationItem, ::handleExploreNavigation) collect(navModel.exploreNavigationItem, ::handleExploreNavigation)
collect(navModel.exploreNavigationArtists, ::handleExplorePicker) collect(navModel.exploreNavigationArtists, ::handleExplorePicker)
collectImmediately(playbackModel.song, ::updateSong) collectImmediately(playbackModel.song, ::updateSong)
collect(playbackModel.artistPlaybackPickerSong, ::handlePlaybackPicker) collect(playbackModel.artistPlaybackPickerSong, ::handlePlaybackArtistPicker)
} }
override fun onStart() { override fun onStart() {
@ -216,22 +216,25 @@ class MainFragment :
if (playbackModel.song.value == null) { if (playbackModel.song.value == null) {
// Sometimes lingering drags can un-hide the playback sheet even when we intend to // Sometimes lingering drags can un-hide the playback sheet even when we intend to
// hide it, make sure we keep it hidden. // hide it, make sure we keep it hidden.
tryHideAll() tryHideAllSheets()
} }
// Since the callback is also reliant on the bottom sheets, we must also update it // Since the callback is also reliant on the bottom sheets, we must also update it
// every frame. // every frame.
callback.updateEnabledState() callback.invalidateEnabled()
return true return true
} }
private fun handleMainNavigation(action: MainNavigationAction?) { private fun handleMainNavigation(action: MainNavigationAction?) {
if (action == null) return if (action == null) {
// Nothing to do.
return
}
when (action) { when (action) {
is MainNavigationAction.Expand -> tryExpandAll() is MainNavigationAction.Expand -> tryExpandSheets()
is MainNavigationAction.Collapse -> tryCollapseAll() is MainNavigationAction.Collapse -> tryCollapseSheets()
// TODO: Figure out how to clear out the selections as one moves between screens. // TODO: Figure out how to clear out the selections as one moves between screens.
is MainNavigationAction.Directions -> findNavController().navigate(action.directions) is MainNavigationAction.Directions -> findNavController().navigate(action.directions)
} }
@ -241,12 +244,13 @@ class MainFragment :
private fun handleExploreNavigation(item: Music?) { private fun handleExploreNavigation(item: Music?) {
if (item != null) { if (item != null) {
tryCollapseAll() tryCollapseSheets()
} }
} }
private fun handleExplorePicker(items: List<Artist>?) { private fun handleExplorePicker(items: List<Artist>?) {
if (items != null) { if (items != null) {
// Navigate to the analogous artist picker dialog.
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionPickNavigationArtist( MainFragmentDirections.actionPickNavigationArtist(
@ -257,14 +261,15 @@ class MainFragment :
private fun updateSong(song: Song?) { private fun updateSong(song: Song?) {
if (song != null) { if (song != null) {
tryUnhideAll() tryShowSheets()
} else { } else {
tryHideAll() tryHideAllSheets()
} }
} }
private fun handlePlaybackPicker(song: Song?) { private fun handlePlaybackArtistPicker(song: Song?) {
if (song != null) { if (song != null) {
// Navigate to the analogous artist picker dialog.
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionPickPlaybackArtist(song.uid))) MainFragmentDirections.actionPickPlaybackArtist(song.uid)))
@ -272,18 +277,18 @@ class MainFragment :
} }
} }
private fun tryExpandAll() { private fun tryExpandSheets() {
val binding = requireBinding() val binding = requireBinding()
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) { if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
// State is collapsed and non-hidden, expand // Playback sheet is not expanded and not hidden, we can expand it.
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
} }
} }
private fun tryCollapseAll() { private fun tryCollapseSheets() {
val binding = requireBinding() val binding = requireBinding()
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
@ -292,13 +297,12 @@ class MainFragment :
// Make sure the queue is also collapsed here. // Make sure the queue is also collapsed here.
val queueSheetBehavior = val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
queueSheetBehavior?.state = NeoBottomSheetBehavior.STATE_COLLAPSED queueSheetBehavior?.state = NeoBottomSheetBehavior.STATE_COLLAPSED
} }
} }
private fun tryUnhideAll() { private fun tryShowSheets() {
val binding = requireBinding() val binding = requireBinding()
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
@ -318,7 +322,7 @@ class MainFragment :
} }
} }
private fun tryHideAll() { private fun tryHideAllSheets() {
val binding = requireBinding() val binding = requireBinding()
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
@ -342,8 +346,8 @@ class MainFragment :
} }
/** /**
* A back press callback that handles how to respond to backwards navigation in the detail * A [OnBackPressedCallback] that overrides the back button to first navigate out of
* fragments and the playback panel. * internal app components, such as the Bottom Sheets or Explore Navigation.
*/ */
private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) { private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@ -353,36 +357,45 @@ class MainFragment :
val queueSheetBehavior = val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
// If expanded, collapse the queue sheet first.
if (queueSheetBehavior != null && if (queueSheetBehavior != null &&
queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED && queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) { playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
// Collapse the queue first if it is expanded.
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
return return
} }
// If expanded, collapse the playback sheet next.
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED && if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) { playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
// Then collapse the playback sheet.
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
return return
} }
// Then try to navigate out of the explore navigation fragments (i.e Detail Views)
binding.exploreNavHost.findNavController().navigateUp() binding.exploreNavHost.findNavController().navigateUp()
} }
fun updateEnabledState() { /**
* Force this instance to update whether it's enabled or not. If there are no app
* components that the back button should close first, the instance is disabled and
* back navigation is delegated to the system.
*
* Normally, this callback would have just called the [MainActivity.onBackPressed]
* if there were no components to close, but that prevents adaptive back navigation
* from working on Android 14+, so we must do it this way.
*/
fun invalidateEnabled() {
val binding = requireBinding() val binding = requireBinding()
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
val queueSheetBehavior = val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
val exploreNavController = binding.exploreNavHost.findNavController() val exploreNavController = binding.exploreNavHost.findNavController()
isEnabled = isEnabled =
queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED || playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
exploreNavController.currentDestination?.id != exploreNavController.currentDestination?.id !=
exploreNavController.graph.startDestinationId exploreNavController.graph.startDestinationId
} }

View file

@ -175,10 +175,7 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists) navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
} }
/**
* Update the currently displayed [Album].
* @param album The new [Album] to display. Null if there is no longer one.
*/
private fun updateAlbum(album: Album?) { private fun updateAlbum(album: Album?) {
if (album == null) { if (album == null) {
// Album we were showing no longer exists. // Album we were showing no longer exists.
@ -189,12 +186,6 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
requireBinding().detailToolbar.title = album.resolveName(requireContext()) requireBinding().detailToolbar.title = album.resolveName(requireContext())
} }
/**
* Update the current playback state in the context of the currently displayed [Album].
* @param song The current [Song] playing.
* @param parent The current [MusicParent] playing, null if all songs.
* @param isPlaying Whether playback is ongoing or paused.
*/
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) { if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
detailAdapter.setPlayingItem(song, isPlaying) detailAdapter.setPlayingItem(song, isPlaying)
@ -204,10 +195,6 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
} }
} }
/**
* Handle a navigation event.
* @param item The [Music] to navigate to, null if there is no item.
*/
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
val binding = requireBinding() val binding = requireBinding()
when (item) { when (item) {
@ -216,7 +203,7 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
is Song -> { is Song -> {
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) { if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) {
logD("Navigating to a song in this album") logD("Navigating to a song in this album")
scrollToItem(item) scrollToAlbumSong(item)
navModel.finishExploreNavigation() navModel.finishExploreNavigation()
} else { } else {
logD("Navigating to another album") logD("Navigating to another album")
@ -250,11 +237,7 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
} }
} }
/** private fun scrollToAlbumSong(song: Song) {
* Scroll to a [Song] within the [Album] view, assuming that it is present.
* @param song The song to try to scroll to.
*/
private fun scrollToItem(song: Song) {
// Calculate where the item for the currently played song is // Calculate where the item for the currently played song is
val pos = detailModel.albumList.value.indexOf(song) val pos = detailModel.albumList.value.indexOf(song)
@ -293,10 +276,6 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
} }
} }
/**
* Update the current item selection.
* @param selected The list of selected items.
*/
private fun updateSelection(selected: List<Music>) { private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelectedItems(selected) detailAdapter.setSelectedItems(selected)
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)

View file

@ -179,10 +179,6 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
} }
} }
/**
* Update the currently displayed [Artist]
* @param artist The new [Artist] to display. Null if there is no longer one.
*/
private fun updateItem(artist: Artist?) { private fun updateItem(artist: Artist?) {
if (artist == null) { if (artist == null) {
// Artist we were showing no longer exists. // Artist we were showing no longer exists.
@ -193,17 +189,11 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
requireBinding().detailToolbar.title = artist.resolveName(requireContext()) requireBinding().detailToolbar.title = artist.resolveName(requireContext())
} }
/**
* Update the current playback state in the context of the currently displayed [Artist].
* @param song The current [Song] playing.
* @param parent The current [MusicParent] playing, null if all songs.
* @param isPlaying Whether playback is ongoing or paused.
*/
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value) val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value)
val playingItem = val playingItem =
when (parent) { when (parent) {
// Always highlight a playing album from this artist. // Always highlight a playing album if it's from this artist.
is Album -> parent is Album -> parent
// If the parent is the artist itself, use the currently playing song. // If the parent is the artist itself, use the currently playing song.
currentArtist -> song currentArtist -> song
@ -214,10 +204,6 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
detailAdapter.setPlayingItem(playingItem, isPlaying) detailAdapter.setPlayingItem(playingItem, isPlaying)
} }
/**
* Handle a navigation event.
* @param item The [Music] to navigate to, null if there is no item.
*/
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
val binding = requireBinding() val binding = requireBinding()
@ -253,10 +239,6 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
} }
} }
/**
* Update the current item selection.
* @param selected The list of selected items.
*/
private fun updateSelection(selected: List<Music>) { private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelectedItems(selected) detailAdapter.setSelectedItems(selected)
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)

View file

@ -61,8 +61,7 @@ class DetailViewModel(application: Application) :
private val _currentSong = MutableStateFlow<DetailSong?>(null) private val _currentSong = MutableStateFlow<DetailSong?>(null)
/** /**
* The current [Song] that should be displayed in the [Song] detail view. Null if there * The current [DetailSong] to display. Null if there is nothing to show.
* is no [Song].
* TODO: De-couple Song and Properties? * TODO: De-couple Song and Properties?
*/ */
val currentSong: StateFlow<DetailSong?> val currentSong: StateFlow<DetailSong?>
@ -71,23 +70,16 @@ class DetailViewModel(application: Application) :
// --- ALBUM --- // --- ALBUM ---
private val _currentAlbum = MutableStateFlow<Album?>(null) private val _currentAlbum = MutableStateFlow<Album?>(null)
/** /** The current [Album] to display. Null if there is nothing to show. */
* The current [Album] that should be displayed in the [Album] detail view. Null if there
* is no [Album].
*/
val currentAlbum: StateFlow<Album?> val currentAlbum: StateFlow<Album?>
get() = _currentAlbum get() = _currentAlbum
private val _albumData = MutableStateFlow(listOf<Item>()) private val _albumList = MutableStateFlow(listOf<Item>())
/** /** The current list data derived from [currentAlbum]. */
* The current list data derived from [currentAlbum], for use in the [Album] detail view.
*/
val albumList: StateFlow<List<Item>> val albumList: StateFlow<List<Item>>
get() = _albumData get() = _albumList
/** /** The current [Sort] used for [Song]s in [albumList]. */
* The current [Sort] used for [Song]s in the [Album] detail view.
*/
var albumSort: Sort var albumSort: Sort
get() = settings.detailAlbumSort get() = settings.detailAlbumSort
set(value) { set(value) {
@ -99,22 +91,15 @@ class DetailViewModel(application: Application) :
// --- ARTIST --- // --- ARTIST ---
private val _currentArtist = MutableStateFlow<Artist?>(null) private val _currentArtist = MutableStateFlow<Artist?>(null)
/** /** The current [Artist] to display. Null if there is nothing to show. */
* The current [Artist] that should be displayed in the [Artist] detail view. Null if there
* is no [Artist].
*/
val currentArtist: StateFlow<Artist?> val currentArtist: StateFlow<Artist?>
get() = _currentArtist get() = _currentArtist
private val _artistData = MutableStateFlow(listOf<Item>()) private val _artistList = MutableStateFlow(listOf<Item>())
/** /** The current list derived from [currentArtist]. */
* The current list derived from [currentArtist], for use in the [Artist] detail view. val artistList: StateFlow<List<Item>> = _artistList
*/
val artistList: StateFlow<List<Item>> = _artistData
/** /** The current [Sort] used for [Song]s in [artistList]. */
* The current [Sort] used for [Song]s in the [Artist] detail view.
*/
var artistSort: Sort var artistSort: Sort
get() = settings.detailArtistSort get() = settings.detailArtistSort
set(value) { set(value) {
@ -126,22 +111,15 @@ class DetailViewModel(application: Application) :
// --- GENRE --- // --- GENRE ---
private val _currentGenre = MutableStateFlow<Genre?>(null) private val _currentGenre = MutableStateFlow<Genre?>(null)
/** /** The current [Genre] to display. Null if there is nothing to show. */
* The current [Genre] that should be displayed in the [Genre] detail view. Null if there
* is no [Genre].
*/
val currentGenre: StateFlow<Genre?> val currentGenre: StateFlow<Genre?>
get() = _currentGenre get() = _currentGenre
private val _genreData = MutableStateFlow(listOf<Item>()) private val _genreList = MutableStateFlow(listOf<Item>())
/** /** The current list data derived from [currentGenre]. */
* The current list data derived from [currentGenre], for use in the [Genre] detail view. val genreList: StateFlow<List<Item>> = _genreList
*/
val genreList: StateFlow<List<Item>> = _genreData
/** /** The current [Sort] used for [Song]s in [genreList]. */
* The current [Sort] used for [Song]s in the [Genre] detail view.
*/
var genreSort: Sort var genreSort: Sort
get() = settings.detailGenreSort get() = settings.detailGenreSort
set(value) { set(value) {
@ -254,14 +232,6 @@ class DetailViewModel(application: Application) :
_currentGenre.value = requireMusic<Genre>(uid).also { refreshGenreList(it) } _currentGenre.value = requireMusic<Genre>(uid).also { refreshGenreList(it) }
} }
/**
* A wrapper around [MusicStore.Library.find] that asserts that the returned data should
* be valid.
* @param T The type of music that should be found
* @param uid The [Music.UID] of the [T] to find
* @return A [T] with the given [Music.UID]
* @throws IllegalStateException If nothing can be found
*/
private fun <T: Music> requireMusic(uid: Music.UID): T = private fun <T: Music> requireMusic(uid: Music.UID): T =
requireNotNull(unlikelyToBeNull(musicStore.library).find(uid)) { "Invalid id provided" } requireNotNull(unlikelyToBeNull(musicStore.library).find(uid)) { "Invalid id provided" }
@ -275,20 +245,13 @@ class DetailViewModel(application: Application) :
_currentSong.value = DetailSong(song, null) _currentSong.value = DetailSong(song, null)
currentSongJob = currentSongJob =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val info = loadSongProperties(song) val info = loadProperties(song)
yield() yield()
_currentSong.value = DetailSong(song, info) _currentSong.value = DetailSong(song, info)
} }
} }
/** private fun loadProperties(song: Song): DetailSong.Properties {
* Load a new set of [DetailSong.Properties] based on the given [Song]'s file using
* [MediaExtractor].
* @param song The song to load the properties from.
* @return A [DetailSong.Properties] containing the properties that could be
* extracted from the file.
*/
private fun loadSongProperties(song: Song): DetailSong.Properties {
// While we would use ExoPlayer to extract this information, it doesn't support // While we would use ExoPlayer to extract this information, it doesn't support
// common data like bit rate in progressive data sources due to there being no // common data like bit rate in progressive data sources due to there being no
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor. // demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
@ -349,10 +312,6 @@ class DetailViewModel(application: Application) :
return DetailSong.Properties(bitrate, sampleRate, resolvedMimeType) return DetailSong.Properties(bitrate, sampleRate, resolvedMimeType)
} }
/**
* Refresh [albumList] to reflect the given [Album] and any [Sort] changes.
* @param album The [Album] to create the list from.
*/
private fun refreshAlbumList(album: Album) { private fun refreshAlbumList(album: Album) {
logD("Refreshing album data") logD("Refreshing album data")
val data = mutableListOf<Item>(album) val data = mutableListOf<Item>(album)
@ -374,13 +333,9 @@ class DetailViewModel(application: Application) :
data.addAll(songs) data.addAll(songs)
} }
_albumData.value = data _albumList.value = data
} }
/**
* Refresh [artistList] to reflect the given [Artist] and any [Sort] changes.
* @param artist The [Artist] to create the list from.
*/
private fun refreshArtistList(artist: Artist) { private fun refreshArtistList(artist: Artist) {
logD("Refreshing artist data") logD("Refreshing artist data")
val data = mutableListOf<Item>(artist) val data = mutableListOf<Item>(artist)
@ -421,13 +376,9 @@ class DetailViewModel(application: Application) :
data.addAll(artistSort.songs(artist.songs)) data.addAll(artistSort.songs(artist.songs))
} }
_artistData.value = data.toList() _artistList.value = data.toList()
} }
/**
* Refresh [genreList] to reflect the given [Genre] and any [Sort] changes.
* @param genre The [Genre] to create the list from.
*/
private fun refreshGenreList(genre: Genre) { private fun refreshGenreList(genre: Genre) {
logD("Refreshing genre data") logD("Refreshing genre data")
val data = mutableListOf<Item>(genre) val data = mutableListOf<Item>(genre)
@ -436,7 +387,7 @@ class DetailViewModel(application: Application) :
data.addAll(genre.artists) data.addAll(genre.artists)
data.add(SortHeader(R.string.lbl_songs)) data.add(SortHeader(R.string.lbl_songs))
data.addAll(genreSort.songs(genre.songs)) data.addAll(genreSort.songs(genre.songs))
_genreData.value = data _genreList.value = data
} }
/** /**

View file

@ -174,10 +174,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
} }
} }
/**
* Update the currently displayed [Genre]
* @param genre The new [Genre] to display. Null if there is no longer one.
*/
private fun updateItem(genre: Genre?) { private fun updateItem(genre: Genre?) {
if (genre == null) { if (genre == null) {
// Genre we were showing no longer exists. // Genre we were showing no longer exists.
@ -188,12 +184,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
requireBinding().detailToolbar.title = genre.resolveName(requireContext()) requireBinding().detailToolbar.title = genre.resolveName(requireContext())
} }
/**
* Update the current playback state in the context of the currently displayed [Genre].
* @param song The current [Song] playing.
* @param parent The current [MusicParent] playing, null if all songs.
* @param isPlaying Whether playback is ongoing or paused.
*/
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
var item: Item? = null var item: Item? = null
@ -208,10 +198,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
detailAdapter.setPlayingItem(item, isPlaying) detailAdapter.setPlayingItem(item, isPlaying)
} }
/**
* Handle a navigation event.
* @param item The [Music] to navigate to, null if there is no item.
*/
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
when (item) { when (item) {
is Song -> { is Song -> {
@ -236,10 +222,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
} }
} }
/**
* Update the current item selection.
* @param selected The list of selected items.
*/
private fun updateSelection(selected: List<Music>) { private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelectedItems(selected) detailAdapter.setSelectedItems(selected)
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)

View file

@ -39,7 +39,6 @@ constructor(
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.editTextStyle defStyleAttr: Int = R.attr.editTextStyle
) : TextInputEditText(context, attrs, defStyleAttr) { ) : TextInputEditText(context, attrs, defStyleAttr) {
init { init {
// Enable selection, but still disable focus (i.e Keyboard opening) // Enable selection, but still disable focus (i.e Keyboard opening)
setTextIsSelectable(true) setTextIsSelectable(true)
@ -50,10 +49,8 @@ constructor(
// Make text immutable // Make text immutable
override fun getFreezesText() = false override fun getFreezesText() = false
// Prevent editing by default // Prevent editing by default
override fun getDefaultEditable() = false override fun getDefaultEditable() = false
// Remove the movement method that allows cursor scrolling // Remove the movement method that allows cursor scrolling
override fun getDefaultMovementMethod() = null override fun getDefaultMovementMethod() = null
} }

View file

@ -56,10 +56,6 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
collectImmediately(detailModel.currentSong, ::updateSong) collectImmediately(detailModel.currentSong, ::updateSong)
} }
/**
* Update the currently displayed song.
* @param song The [DetailViewModel.DetailSong] to display. Null if there is no longer one.
*/
private fun updateSong(song: DetailSong?) { private fun updateSong(song: DetailSong?) {
val binding = requireBinding() val binding = requireBinding()
@ -71,19 +67,16 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
if (song.properties != null) { if (song.properties != null) {
// Finished loading Song properties, populate and show the list of Song information. // Finished loading Song properties, populate and show the list of Song information.
binding.detailLoading.isInvisible = true
binding.detailContainer.isInvisible = false
val context = requireContext() val context = requireContext()
// File name
binding.detailFileName.setText(song.song.path.name) binding.detailFileName.setText(song.song.path.name)
// Relative (Parent) directory
binding.detailRelativeDir.setText(song.song.path.parent.resolveName(context)) binding.detailRelativeDir.setText(song.song.path.parent.resolveName(context))
// Format
binding.detailFormat.setText(song.properties.resolvedMimeType.resolveName(context)) binding.detailFormat.setText(song.properties.resolvedMimeType.resolveName(context))
// Size
binding.detailSize.setText(Formatter.formatFileSize(context, song.song.size)) binding.detailSize.setText(Formatter.formatFileSize(context, song.song.size))
// Duration
binding.detailDuration.setText(song.song.durationMs.formatDurationMs(true)) binding.detailDuration.setText(song.song.durationMs.formatDurationMs(true))
// Bit rate (if present)
if (song.properties.bitrateKbps != null) { if (song.properties.bitrateKbps != null) {
binding.detailBitrate.setText( binding.detailBitrate.setText(
getString(R.string.fmt_bitrate, song.properties.bitrateKbps)) getString(R.string.fmt_bitrate, song.properties.bitrateKbps))
@ -91,16 +84,12 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
binding.detailBitrate.setText(R.string.def_bitrate) binding.detailBitrate.setText(R.string.def_bitrate)
} }
// Sample rate (if present)
if (song.properties.sampleRateHz != null) { if (song.properties.sampleRateHz != null) {
binding.detailSampleRate.setText( binding.detailSampleRate.setText(
getString(R.string.fmt_sample_rate, song.properties.sampleRateHz)) getString(R.string.fmt_sample_rate, song.properties.sampleRateHz))
} else { } else {
binding.detailSampleRate.setText(R.string.def_sample_rate) binding.detailSampleRate.setText(R.string.def_sample_rate)
} }
binding.detailLoading.isInvisible = true
binding.detailContainer.isInvisible = false
} else { } else {
// Loading is still on-going, don't show anything yet. // Loading is still on-going, don't show anything yet.
binding.detailLoading.isInvisible = false binding.detailLoading.isInvisible = false

View file

@ -40,7 +40,7 @@ import org.oxycblt.auxio.util.inflater
/** /**
* An [DetailAdapter] implementing the header and sub-items for the [Album] detail view. * An [DetailAdapter] implementing the header and sub-items for the [Album] detail view.
* @param listener A [Listener] for list interactions. * @param listener A [Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) { class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {

View file

@ -39,7 +39,7 @@ import org.oxycblt.auxio.util.inflater
/** /**
* A [DetailAdapter] implementing the header and sub-items for the [Artist] detail view. * A [DetailAdapter] implementing the header and sub-items for the [Artist] detail view.
* @param listener A [DetailAdapter.Listener] for list interactions. * @param listener A [DetailAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) { class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {

View file

@ -35,13 +35,13 @@ import org.oxycblt.auxio.util.inflater
/** /**
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters. * A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
* @param callback A [Listener] for list interactions. * @param listener A [Listener] to bind interactions to.
* @param itemCallback A [DiffUtil.ItemCallback] to use with [AsyncListDiffer] when updating the * @param itemCallback A [DiffUtil.ItemCallback] to use with [AsyncListDiffer] when updating the
* internal list. * internal list.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
abstract class DetailAdapter( abstract class DetailAdapter(
private val callback: Listener, private val listener: Listener,
itemCallback: DiffUtil.ItemCallback<Item> itemCallback: DiffUtil.ItemCallback<Item>
) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup { ) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
// Safe to leak this since the callback will not fire during initialization // Safe to leak this since the callback will not fire during initialization
@ -67,7 +67,7 @@ abstract class DetailAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = differ.currentList[position]) { when (val item = differ.currentList[position]) {
is Header -> (holder as HeaderViewHolder).bind(item) is Header -> (holder as HeaderViewHolder).bind(item)
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, callback) is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
} }
} }
@ -89,9 +89,7 @@ abstract class DetailAdapter(
differ.submitList(newList) differ.submitList(newList)
} }
/** /** An extended [ExtendedListListener] for [DetailAdapter] implementations. */
* An extended [ExtendedListListener] for [DetailAdapter] implementations.
*/
interface Listener : ExtendedListListener { interface Listener : ExtendedListListener {
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented. // TODO: Split off into sub-listeners if a collapsing toolbar is implemented.
/** /**

View file

@ -37,7 +37,7 @@ import org.oxycblt.auxio.util.inflater
/** /**
* An [DetailAdapter] implementing the header and sub-items for the [Genre] detail view. * An [DetailAdapter] implementing the header and sub-items for the [Genre] detail view.
* @param listener A [DetailAdapter.Listener] for list interactions. * @param listener A [DetailAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) { class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {

View file

@ -93,7 +93,7 @@ class HomeFragment :
// our transitions. // our transitions.
val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_AXIS, -1) val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_AXIS, -1)
if (axis > -1) { if (axis > -1) {
initAxisTransitions(axis) setupAxisTransitions(axis)
} }
} }
} }
@ -199,7 +199,7 @@ class HomeFragment :
// Handle main actions (Search, Settings, About) // Handle main actions (Search, Settings, About)
R.id.action_search -> { R.id.action_search -> {
logD("Navigating to search") logD("Navigating to search")
initAxisTransitions(MaterialSharedAxis.Z) setupAxisTransitions(MaterialSharedAxis.Z)
findNavController().navigate(HomeFragmentDirections.actionShowSearch()) findNavController().navigate(HomeFragmentDirections.actionShowSearch())
} }
R.id.action_settings -> { R.id.action_settings -> {
@ -238,10 +238,6 @@ class HomeFragment :
return true return true
} }
/**
* Set up the TabLayout and [ViewPager2] to reflect the current tab configuration.
* @param binding The [FragmentHomeBinding] to apply the tab configuration to.
*/
private fun setupPager(binding: FragmentHomeBinding) { private fun setupPager(binding: FragmentHomeBinding) {
binding.homePager.adapter = HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner) binding.homePager.adapter = HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner)
@ -264,10 +260,6 @@ class HomeFragment :
AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes)).attach() AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes)).attach()
} }
/**
* Update the UI to reflect the current tab.
* @param tabMode The [MusicMode] of the currently shown tab.
*/
private fun updateCurrentTab(tabMode: MusicMode) { private fun updateCurrentTab(tabMode: MusicMode) {
// Update the sort options to align with those allowed by the tab // Update the sort options to align with those allowed by the tab
val isVisible: (Int) -> Boolean = when (tabMode) { val isVisible: (Int) -> Boolean = when (tabMode) {
@ -310,10 +302,6 @@ class HomeFragment :
requireBinding().homeAppbar.liftOnScrollTargetViewId = getTabRecyclerId(tabMode) requireBinding().homeAppbar.liftOnScrollTargetViewId = getTabRecyclerId(tabMode)
} }
/**
* Handle a request to recreate all home tabs.
* @param recreate Whether to recreate all tabs.
*/
private fun handleRecreate(recreate: Boolean) { private fun handleRecreate(recreate: Boolean) {
if (!recreate) { if (!recreate) {
// Nothing to do // Nothing to do
@ -328,11 +316,6 @@ class HomeFragment :
homeModel.finishRecreate() homeModel.finishRecreate()
} }
/**
* Update the currently displayed [Indexer.State]
* @param state The new [Indexer.State] to show. Null if the state is currently
* indeterminate (Not loading or complete).
*/
private fun updateIndexerState(state: Indexer.State?) { private fun updateIndexerState(state: Indexer.State?) {
val binding = requireBinding() val binding = requireBinding()
when (state) { when (state) {
@ -345,11 +328,6 @@ class HomeFragment :
} }
} }
/**
* Configure the UI to display the given [Indexer.Response].
* @param binding The [FragmentHomeBinding] whose views should be updated.
* @param response The [Indexer.Response] to show.
*/
private fun setupCompleteState(binding: FragmentHomeBinding, response: Indexer.Response) { private fun setupCompleteState(binding: FragmentHomeBinding, response: Indexer.Response) {
if (response is Indexer.Response.Ok) { if (response is Indexer.Response.Ok) {
logD("Received ok response") logD("Received ok response")
@ -401,11 +379,6 @@ class HomeFragment :
} }
} }
/**
* Configure the UI to display the given [Indexer.Indexing] state..
* @param binding The [FragmentHomeBinding] whose views should be updated.
* @param indexing The [Indexer.Indexing] state to show.
*/
private fun setupIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) { private fun setupIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) {
// Remove all content except for the progress indicator. // Remove all content except for the progress indicator.
binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingContainer.visibility = View.VISIBLE
@ -431,11 +404,6 @@ class HomeFragment :
} }
} }
/**
* Update the FAB visibility to reflect the state of other home elements.
* @param songs The current list of songs in the home view.
* @param isFastScrolling Whether the user is currently fast-scrolling.
*/
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) { private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) {
val binding = requireBinding() val binding = requireBinding()
// If there are no songs, it's likely that the library has not been loaded, so // If there are no songs, it's likely that the library has not been loaded, so
@ -448,10 +416,6 @@ class HomeFragment :
} }
} }
/**
* Handle a navigation event.
* @param item The [Music] to navigate to, null if there is no item.
*/
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
val action = val action =
when (item) { when (item) {
@ -462,14 +426,10 @@ class HomeFragment :
else -> return else -> return
} }
initAxisTransitions(MaterialSharedAxis.X) setupAxisTransitions(MaterialSharedAxis.X)
findNavController().navigate(action) findNavController().navigate(action)
} }
/**
* Update the current item selection.
* @param selected The list of selected items.
*/
private fun updateSelection(selected: List<Music>) { private fun updateSelection(selected: List<Music>) {
val binding = requireBinding() val binding = requireBinding()
if (binding.homeSelectionToolbar.updateSelectionAmount(selected.size) && if (binding.homeSelectionToolbar.updateSelectionAmount(selected.size) &&
@ -481,24 +441,7 @@ class HomeFragment :
} }
} }
/** private fun setupAxisTransitions(axis: Int) {
* Get the ID of the RecyclerView contained by a tab.
* @param tabMode The mode of the tab to get the ID from
* @return The ID of the RecyclerView contained by the given tab.
*/
private fun getTabRecyclerId(tabMode: MusicMode) =
when (tabMode) {
MusicMode.SONGS -> R.id.home_song_recycler
MusicMode.ALBUMS -> R.id.home_album_recycler
MusicMode.ARTISTS -> R.id.home_artist_recycler
MusicMode.GENRES -> R.id.home_genre_recycler
}
/**
* Set up [MaterialSharedAxis] transitions.
* @param axis The axis that the transition should occur on, such as [MaterialSharedAxis.X]
*/
private fun initAxisTransitions(axis: Int) {
// Sanity check to avoid in-correct axis transitions // Sanity check to avoid in-correct axis transitions
check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) { check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) {
"Not expecting Y axis transition" "Not expecting Y axis transition"
@ -510,9 +453,23 @@ class HomeFragment :
reenterTransition = MaterialSharedAxis(axis, false) reenterTransition = MaterialSharedAxis(axis, false)
} }
/**
* Get the ID of the RecyclerView contained by [ViewPager2] tab represented with
* the given [MusicMode].
* @param tabMode The [MusicMode] of the tab.
* @return The ID of the RecyclerView contained by the given tab.
*/
private fun getTabRecyclerId(tabMode: MusicMode) =
when (tabMode) {
MusicMode.SONGS -> R.id.home_song_recycler
MusicMode.ALBUMS -> R.id.home_album_recycler
MusicMode.ARTISTS -> R.id.home_artist_recycler
MusicMode.GENRES -> R.id.home_genre_recycler
}
/** /**
* [FragmentStateAdapter] implementation for the [HomeFragment]'s [ViewPager2] instance. * [FragmentStateAdapter] implementation for the [HomeFragment]'s [ViewPager2] instance.
* @param tabs The current tab configuration. This should define the fragments created. * @param tabs The current tab configuration. This will define the [Fragment]s created.
* @param fragmentManager The [FragmentManager] required by [FragmentStateAdapter]. * @param fragmentManager The [FragmentManager] required by [FragmentStateAdapter].
* @param lifecycleOwner The [LifecycleOwner], whose Lifecycle is required by * @param lifecycleOwner The [LifecycleOwner], whose Lifecycle is required by
* [FragmentStateAdapter]. * [FragmentStateAdapter].

View file

@ -77,7 +77,7 @@ class HomeViewModel(application: Application) :
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding * A list of [MusicMode] corresponding to the current [Tab] configuration, excluding
* invisible [Tab]s. * invisible [Tab]s.
*/ */
var currentTabModes: List<MusicMode> = getVisibleTabModes() var currentTabModes: List<MusicMode> = makeTabModes()
private set private set
private val _currentTabMode = MutableStateFlow(currentTabModes[0]) private val _currentTabMode = MutableStateFlow(currentTabModes[0])
@ -133,7 +133,7 @@ class HomeViewModel(application: Application) :
when (key) { when (key) {
context.getString(R.string.set_key_lib_tabs) -> { context.getString(R.string.set_key_lib_tabs) -> {
// Tabs changed, update the current tabs and set up a re-create event. // Tabs changed, update the current tabs and set up a re-create event.
currentTabModes = getVisibleTabModes() currentTabModes = makeTabModes()
_shouldRecreate.value = true _shouldRecreate.value = true
} }
@ -155,7 +155,8 @@ class HomeViewModel(application: Application) :
} }
/** /**
* Mark the recreation process as completed, resetting [shouldRecreate]. * Mark the recreation process as complete.
* @see shouldRecreate
*/ */
fun finishRecreate() { fun finishRecreate() {
_shouldRecreate.value = false _shouldRecreate.value = false
@ -211,9 +212,10 @@ class HomeViewModel(application: Application) :
} }
/** /**
* Get the [MusicMode]s of the visible [Tab]s from the [Tab] configuration. * Create a list of [MusicMode]s representing a simpler version of the [Tab] configuration.
* @return A list of [MusicMode]s for each visible [Tab] in the [Tab] configuration. * @return A list of the [MusicMode]s for each visible [Tab] in the configuration,
* ordered in the same way as the configuration.
*/ */
private fun getVisibleTabModes() = private fun makeTabModes() =
settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode } settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
} }

View file

@ -132,11 +132,6 @@ class AlbumListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
openMusicMenu(anchor, R.menu.menu_album_actions, item) openMusicMenu(anchor, R.menu.menu_album_actions, item)
} }
/**
* Update the current playback state in the context of the current [Album] list.
* @param parent The current [MusicParent] playing, null if all songs.
* @param isPlaying Whether playback is ongoing or paused.
*/
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If an album is playing, highlight it within this adapter. // If an album is playing, highlight it within this adapter.
albumAdapter.setPlayingItem(parent as? Album, isPlaying) albumAdapter.setPlayingItem(parent as? Album, isPlaying)
@ -144,7 +139,7 @@ class AlbumListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
/** /**
* A [SelectionIndicatorAdapter] that shows a list of [Album]s using [AlbumViewHolder]. * A [SelectionIndicatorAdapter] that shows a list of [Album]s using [AlbumViewHolder].
* @param listener An [ExtendedListListener] for list interactions. * @param listener An [ExtendedListListener] to bind interactions to.
*/ */
private class AlbumAdapter(private val listener: ExtendedListListener) : private class AlbumAdapter(private val listener: ExtendedListListener) :
SelectionIndicatorAdapter<AlbumViewHolder>() { SelectionIndicatorAdapter<AlbumViewHolder>() {

View file

@ -107,11 +107,6 @@ class ArtistListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRe
openMusicMenu(anchor, R.menu.menu_artist_actions, item) openMusicMenu(anchor, R.menu.menu_artist_actions, item)
} }
/**
* Update the current playback state in the context of the current [Artist] list.
* @param parent The current [MusicParent] playing, null if all songs.
* @param isPlaying Whether playback is ongoing or paused.
*/
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If an artist is playing, highlight it within this adapter. // If an artist is playing, highlight it within this adapter.
homeAdapter.setPlayingItem(parent as? Artist, isPlaying) homeAdapter.setPlayingItem(parent as? Artist, isPlaying)
@ -119,7 +114,7 @@ class ArtistListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRe
/** /**
* A [SelectionIndicatorAdapter] that shows a list of [Artist]s using [ArtistViewHolder]. * A [SelectionIndicatorAdapter] that shows a list of [Artist]s using [ArtistViewHolder].
* @param listener An [ExtendedListListener] for list interactions. * @param listener An [ExtendedListListener] to bind interactions to.
*/ */
private class ArtistAdapter(private val listener: ExtendedListListener) : private class ArtistAdapter(private val listener: ExtendedListListener) :
SelectionIndicatorAdapter<ArtistViewHolder>() { SelectionIndicatorAdapter<ArtistViewHolder>() {

View file

@ -106,11 +106,6 @@ class GenreListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
openMusicMenu(anchor, R.menu.menu_artist_actions, item) openMusicMenu(anchor, R.menu.menu_artist_actions, item)
} }
/**
* Update the current playback state in the context of the current [Genre] list.
* @param parent The current [MusicParent] playing, null if all songs.
* @param isPlaying Whether playback is ongoing or paused.
*/
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If a genre is playing, highlight it within this adapter. // If a genre is playing, highlight it within this adapter.
homeAdapter.setPlayingItem(parent as? Genre, isPlaying) homeAdapter.setPlayingItem(parent as? Genre, isPlaying)
@ -118,7 +113,7 @@ class GenreListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
/** /**
* A [SelectionIndicatorAdapter] that shows a list of [Genre]s using [GenreViewHolder]. * A [SelectionIndicatorAdapter] that shows a list of [Genre]s using [GenreViewHolder].
* @param listener An [ExtendedListListener] for list interactions. * @param listener An [ExtendedListListener] to bind interactions to.
*/ */
private class GenreAdapter(private val listener: ExtendedListListener) : private class GenreAdapter(private val listener: ExtendedListListener) :
SelectionIndicatorAdapter<GenreViewHolder>() { SelectionIndicatorAdapter<GenreViewHolder>() {

View file

@ -142,11 +142,6 @@ class SongListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecy
openMusicMenu(anchor, R.menu.menu_song_actions, item) openMusicMenu(anchor, R.menu.menu_song_actions, item)
} }
/**
* Update the current playback state in the context of the current [Song] list.
* @param parent The current [MusicParent] playing, null if all songs.
* @param isPlaying Whether playback is ongoing or paused.
*/
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent == null) { if (parent == null) {
homeAdapter.setPlayingItem(song, isPlaying) homeAdapter.setPlayingItem(song, isPlaying)
@ -158,7 +153,7 @@ class SongListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecy
/** /**
* A [SelectionIndicatorAdapter] that shows a list of [Song]s using [SongViewHolder]. * A [SelectionIndicatorAdapter] that shows a list of [Song]s using [SongViewHolder].
* @param listener An [ExtendedListListener] for list interactions. * @param listener An [ExtendedListListener] to bind interactions to.
*/ */
private class SongAdapter(private val listener: ExtendedListListener) : private class SongAdapter(private val listener: ExtendedListListener) :
SelectionIndicatorAdapter<SongViewHolder>() { SelectionIndicatorAdapter<SongViewHolder>() {

View file

@ -54,20 +54,17 @@ sealed class Tab(open val mode: MusicMode) {
// Where V is a bit representing the visibility and T is a 3-bit integer representing the // Where V is a bit representing the visibility and T is a 3-bit integer representing the
// MusicMode for this tab. // MusicMode for this tab.
/** /** The length a well-formed tab sequence should be. */
* The length a well-formed tab sequence should be
*/
private const val SEQUENCE_LEN = 4 private const val SEQUENCE_LEN = 4
/** /**
* The default tab sequence, in integer form. * The default tab sequence, in integer form.
* This will be SONGS, ALBUMS, ARTISTS, GENRES, PLAYLISTS. * This represents a set of four visible tabs ordered as "Song", "Album", "Artist", and
* "Genre".
*/ */
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100 const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
/** /** Maps between the integer code in the tab sequence and it's [MusicMode]. */
* Maps between the integer code in the tab sequence and the actual [MusicMode] instance.
*/
private val MODE_TABLE = private val MODE_TABLE =
arrayOf(MusicMode.SONGS, MusicMode.ALBUMS, MusicMode.ARTISTS, MusicMode.GENRES) arrayOf(MusicMode.SONGS, MusicMode.ALBUMS, MusicMode.ARTISTS, MusicMode.GENRES)
@ -82,7 +79,6 @@ sealed class Tab(open val mode: MusicMode) {
var sequence = 0b0100 var sequence = 0b0100
var shift = SEQUENCE_LEN * 4 var shift = SEQUENCE_LEN * 4
for (tab in distinct) { for (tab in distinct) {
val bin = val bin =
when (tab) { when (tab) {

View file

@ -33,34 +33,12 @@ import org.oxycblt.auxio.util.inflater
* @param listener A [Listener] for tab interactions. * @param listener A [Listener] for tab interactions.
*/ */
class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewHolder>() { class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewHolder>() {
/** /** The current array of [Tab]s. */
* A listener for interactions specific to tab configuration.
*/
interface Listener {
/**
* Called when a tab is clicked, requesting that the visibility should be inverted
* (i.e Visible -> Invisible and vice versa).
* @param tabMode The [MusicMode] of the tab clicked.
*/
fun onToggleVisibility(tabMode: MusicMode)
/**
* Called when the drag handle is pressed, requesting that a drag should be started.
* @param viewHolder The [RecyclerView.ViewHolder] to start dragging.
*/
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
}
/**
* The current array of [Tab]s.
*/
var tabs = arrayOf<Tab>() var tabs = arrayOf<Tab>()
private set private set
override fun getItemCount() = tabs.size override fun getItemCount() = tabs.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.new(parent) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.new(parent)
override fun onBindViewHolder(holder: TabViewHolder, position: Int) { override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
holder.bind(tabs[position], listener) holder.bind(tabs[position], listener)
} }
@ -86,7 +64,7 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
} }
/** /**
* Swap two tabs with eachother. * Swap two tabs with each other.
* @param a The position of the first tab to swap. * @param a The position of the first tab to swap.
* @param b The position of the second tab to swap. * @param b The position of the second tab to swap.
*/ */
@ -97,6 +75,22 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
notifyItemMoved(a, b) notifyItemMoved(a, b)
} }
/** A listener for interactions specific to tab configuration. */
interface Listener {
/**
* Called when a tab is clicked, requesting that the visibility should be inverted
* (i.e Visible -> Invisible and vice versa).
* @param tabMode The [MusicMode] of the tab clicked.
*/
fun onToggleVisibility(tabMode: MusicMode)
/**
* Called when the drag handle is pressed, requesting that a drag should be started.
* @param viewHolder The [RecyclerView.ViewHolder] to start dragging.
*/
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
}
companion object { companion object {
private val PAYLOAD_TAB_CHANGED = Any() private val PAYLOAD_TAB_CHANGED = Any()
} }

View file

@ -109,6 +109,6 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
} }
companion object { companion object {
const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS" private const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS"
} }
} }

View file

@ -39,14 +39,10 @@ import org.oxycblt.auxio.music.Song
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class BitmapProvider(private val context: Context) { class BitmapProvider(private val context: Context) {
/** /** An extension of [Disposable] with an additional [Target] to deliver the final [Bitmap] to. */
* An extension of [Disposable] with an additional [Target] to deliver the final [Bitmap] to.
*/
private data class Request(val disposable: Disposable, val callback: Target) private data class Request(val disposable: Disposable, val callback: Target)
/** /** The target that will receive the requested [Bitmap]. */
* The target that will recieve the requested [Bitmap].
*/
interface Target { interface Target {
/** /**
* Configure the [ImageRequest.Builder] to enable [Target]-specific configuration. * Configure the [ImageRequest.Builder] to enable [Target]-specific configuration.
@ -65,11 +61,7 @@ class BitmapProvider(private val context: Context) {
} }
private var currentRequest: Request? = null private var currentRequest: Request? = null
// Keeps track of the current image request we are on. If the stored handle in an
// ImageRequest is still equal to this, it means that the request has not been
// superceded by a new one.
private var currentHandle = 0L private var currentHandle = 0L
private var handleLock = Any()
/** If this provider is currently attempting to load something. */ /** If this provider is currently attempting to load something. */
val isBusy: Boolean val isBusy: Boolean
@ -82,13 +74,12 @@ class BitmapProvider(private val context: Context) {
*/ */
@Synchronized @Synchronized
fun load(song: Song, target: Target) { fun load(song: Song, target: Target) {
// Increment the handle, indicating a newer request being created. // Increment the handle, indicating a newer request has been created
val handle = synchronized(handleLock) { ++currentHandle } val handle = ++currentHandle
// Be even safer and cancel the previous request.
currentRequest?.run { disposable.dispose() } currentRequest?.run { disposable.dispose() }
currentRequest = null currentRequest = null
val request = val imageRequest =
target.onConfigRequest( target.onConfigRequest(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(song) .data(song)
@ -99,31 +90,32 @@ class BitmapProvider(private val context: Context) {
// callback. // callback.
.target( .target(
onSuccess = { onSuccess = {
synchronized(handleLock) { synchronized(this) {
if (currentHandle == handle) { if (currentHandle == handle) {
// Still the active request, deliver it to the target. // Has not been superceded by a new request, can deliver
// this result.
target.onCompleted(it.toBitmap()) target.onCompleted(it.toBitmap())
} }
} }
}, },
onError = { onError = {
synchronized(handleLock) { synchronized(this) {
if (currentHandle == handle) { if (currentHandle == handle) {
// Still the active request, deliver it to the target. // Has not been superceded by a new request, can deliver
// this result.
target.onCompleted(null) target.onCompleted(null)
} }
} }
}) })
currentRequest = Request(context.imageLoader.enqueue(request.build()), target) currentRequest = Request(context.imageLoader.enqueue(imageRequest.build()), target)
} }
/** /**
* Release this instance. Run this when the object is no longer used to prevent * Release this instance, cancelling any currently running operations.
* stray loading callbacks.
*/ */
@Synchronized @Synchronized
fun release() { fun release() {
synchronized(handleLock) { ++currentHandle } ++currentHandle
currentRequest?.run { disposable.dispose() } currentRequest?.run { disposable.dispose() }
currentRequest = null currentRequest = null
} }

View file

@ -24,17 +24,11 @@ import org.oxycblt.auxio.IntegerTable
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
enum class CoverMode { enum class CoverMode {
/** /** Do not load album covers ("Off"). */
* Do not load album covers ("Off").
*/
OFF, OFF,
/** /** Load covers from the fast, but lower-quality media store database ("Fast"). */
* Load covers from the fast, but lower-quality media store database ("Fast").
*/
MEDIA_STORE, MEDIA_STORE,
/** /** Load high-quality covers directly from music files ("Quality"). */
* Load high-quality covers directly from music files ("Quality").
*/
QUALITY; QUALITY;
/** /**

View file

@ -65,8 +65,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val cornerRadius: Float private val cornerRadius: Float
init { init {
// Android wants you to make separate attributes for each view type, but will // Obtain some StyledImageView attributes to use later when theming the cusotm view.
// then throw an error if you do because of duplicate attribute names.
@SuppressLint("CustomViewStyleable") @SuppressLint("CustomViewStyleable")
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView) val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
// Keep track of our corner radius so that we can apply the same attributes to the custom // Keep track of our corner radius so that we can apply the same attributes to the custom
@ -123,7 +122,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
// Initialize each component before this view is drawn. // Initialize each component before this view is drawn.
invalidateAlpha() invalidateImageAlpha()
invalidatePlayingIndicator() invalidatePlayingIndicator()
invalidateSelectionIndicator() invalidateSelectionIndicator()
} }
@ -135,13 +134,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
override fun setEnabled(enabled: Boolean) { override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled) super.setEnabled(enabled)
invalidateAlpha() invalidateImageAlpha()
invalidatePlayingIndicator() invalidatePlayingIndicator()
} }
override fun setSelected(selected: Boolean) { override fun setSelected(selected: Boolean) {
super.setSelected(selected) super.setSelected(selected)
invalidateAlpha() invalidateImageAlpha()
invalidatePlayingIndicator() invalidatePlayingIndicator()
} }
@ -185,18 +184,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
playbackIndicatorView.isPlaying = value playbackIndicatorView.isPlaying = value
} }
/** private fun invalidateImageAlpha() {
* Invalidate the overall opacity of this view.
*/
private fun invalidateAlpha() {
// If this view is disabled, show it at half-opacity, *unless* it is also marked // If this view is disabled, show it at half-opacity, *unless* it is also marked
// as playing, in which we still want to show it at full-opacity. // as playing, in which we still want to show it at full-opacity.
alpha = if (isSelected || isEnabled) 1f else 0.5f alpha = if (isSelected || isEnabled) 1f else 0.5f
} }
/**
* Invalidate the view's playing ([isSelected]) indicator.
*/
private fun invalidatePlayingIndicator() { private fun invalidatePlayingIndicator() {
if (isSelected) { if (isSelected) {
// View is "selected" (actually marked as playing), so show the playing indicator // View is "selected" (actually marked as playing), so show the playing indicator
@ -213,22 +206,18 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
} }
/**
* Invalidate the view's selection ([isActivated]) indicator, animating it from invisible
* to visible (or vice versa).
*/
private fun invalidateSelectionIndicator() { private fun invalidateSelectionIndicator() {
// Set up a target transition for the selection indicator. // Set up a target transition for the selection indicator.
val targetAlpha: Float val targetAlpha: Float
val targetDuration: Long val targetDuration: Long
if (isActivated) { if (isActivated) {
// Activated -> Show selection indicator // View is "activated" (i.e marked as selected), so show the selection indicator.
targetAlpha = 1f targetAlpha = 1f
targetDuration = targetDuration =
context.getInteger(R.integer.anim_fade_enter_duration).toLong() context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else { } else {
// Activated -> Hide selection indicator. // View is not "activated", hide the selection indicator.
targetAlpha = 0f targetAlpha = 0f
targetDuration = targetDuration =
context.getInteger(R.integer.anim_fade_exit_duration).toLong() context.getInteger(R.integer.anim_fade_exit_duration).toLong()

View file

@ -45,18 +45,13 @@ class PlaybackIndicatorView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
AppCompatImageView(context, attrs, defStyleAttr) { AppCompatImageView(context, attrs, defStyleAttr) {
// The playing drawable will cycle through an active equalizer animation.
private val playingIndicatorDrawable = private val playingIndicatorDrawable =
context.getDrawableCompat(R.drawable.ic_playing_indicator_24) as AnimationDrawable context.getDrawableCompat(R.drawable.ic_playing_indicator_24) as AnimationDrawable
// The paused drawable will be a static drawable of an inactive equalizer.
private val pausedIndicatorDrawable = private val pausedIndicatorDrawable =
context.getDrawableCompat(R.drawable.ic_paused_indicator_24) context.getDrawableCompat(R.drawable.ic_paused_indicator_24)
// Required transformation matrices for the drawables.
private val indicatorMatrix = Matrix() private val indicatorMatrix = Matrix()
private val indicatorMatrixSrc = RectF() private val indicatorMatrixSrc = RectF()
private val indicatorMatrixDst = RectF() private val indicatorMatrixDst = RectF()
private val settings = Settings(context) private val settings = Settings(context)
/** /**

View file

@ -66,17 +66,13 @@ private constructor(private val context: Context, private val album: Album) : Fe
dataSource = DataSource.DISK) dataSource = DataSource.DISK)
} }
/** /** A [Fetcher.Factory] implementation that works with [Song]s.*/
* A [Fetcher.Factory] implementation that works with [Song]s.
*/
class SongFactory : Fetcher.Factory<Song> { class SongFactory : Fetcher.Factory<Song> {
override fun create(data: Song, options: Options, imageLoader: ImageLoader) = override fun create(data: Song, options: Options, imageLoader: ImageLoader) =
AlbumCoverFetcher(options.context, data.album) AlbumCoverFetcher(options.context, data.album)
} }
/** /** A [Fetcher.Factory] implementation that works with [Album]s. */
* A [Fetcher.Factory] implementation that works with [Album]s.
*/
class AlbumFactory : Fetcher.Factory<Album> { class AlbumFactory : Fetcher.Factory<Album> {
override fun create(data: Album, options: Options, imageLoader: ImageLoader) = override fun create(data: Album, options: Options, imageLoader: ImageLoader) =
AlbumCoverFetcher(options.context, data) AlbumCoverFetcher(options.context, data)
@ -100,9 +96,7 @@ private constructor(
return Images.createMosaic(context, results, size) return Images.createMosaic(context, results, size)
} }
/** /** [Fetcher.Factory] implementation. */
* [Fetcher.Factory] implementation.
*/
class Factory : Fetcher.Factory<Artist> { class Factory : Fetcher.Factory<Artist> {
override fun create(data: Artist, options: Options, imageLoader: ImageLoader) = override fun create(data: Artist, options: Options, imageLoader: ImageLoader) =
ArtistImageFetcher(options.context, options.size, data) ArtistImageFetcher(options.context, options.size, data)
@ -124,6 +118,7 @@ private constructor(
return Images.createMosaic(context, results, size) return Images.createMosaic(context, results, size)
} }
/** [Fetcher.Factory] implementation. */
class Factory : Fetcher.Factory<Genre> { class Factory : Fetcher.Factory<Genre> {
override fun create(data: Genre, options: Options, imageLoader: ImageLoader) = override fun create(data: Genre, options: Options, imageLoader: ImageLoader) =
GenreImageFetcher(options.context, options.size, data) GenreImageFetcher(options.context, options.size, data)

View file

@ -26,11 +26,10 @@ import coil.transition.Transition
import coil.transition.TransitionTarget import coil.transition.TransitionTarget
/** /**
* A copy of [CrossfadeTransition.Factory] that applies a transition to error results. You know. * A copy of [CrossfadeTransition.Factory] that also applies a transition to error results.
* Like they used to.
* @author Coil Team, Alexander Capehart (OxygenCobalt) * @author Coil Team, Alexander Capehart (OxygenCobalt)
*/ */
class ErrorCrossfadeTransitionFractory : Transition.Factory { class ErrorCrossfadeTransitionFactory : Transition.Factory {
override fun create(target: TransitionTarget, result: ImageResult): Transition { override fun create(target: TransitionTarget, result: ImageResult): Transition {
// Don't animate if the request was fulfilled by the memory cache. // Don't animate if the request was fulfilled by the memory cache.
if (result is SuccessResult && result.dataSource == DataSource.MEMORY_CACHE) { if (result is SuccessResult && result.dataSource == DataSource.MEMORY_CACHE) {

View file

@ -44,9 +44,7 @@ object Images {
} }
} }
// Use whatever size coil gives us to create the mosaic, rounding it to even so that we // Use whatever size coil gives us to create the mosaic.
// get a symmetrical mosaic [and to prevent bugs]. If there is no size, default to a
// 512x512 mosaic.
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize()) val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
val mosaicFrameSize = val mosaicFrameSize =
Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2)) Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2))
@ -65,16 +63,13 @@ object Images {
break break
} }
// Run the bitmap through a transform to make sure it's a square of the desired // Run the bitmap through a transform to reflect the configuration of other images.
// resolution.
val bitmap = val bitmap =
SquareFrameTransform.INSTANCE.transform( SquareFrameTransform.INSTANCE.transform(
BitmapFactory.decodeStream(stream), mosaicFrameSize) BitmapFactory.decodeStream(stream), mosaicFrameSize)
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null) canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
x += bitmap.width x += bitmap.width
if (x == mosaicSize.width) { if (x == mosaicSize.width) {
x = 0 x = 0
y += bitmap.height y += bitmap.height
@ -90,6 +85,11 @@ object Images {
dataSource = DataSource.DISK) dataSource = DataSource.DISK)
} }
/**
* Get an image dimension suitable to create a mosaic with.
* @return A pixel dimension derived from the given [Dimension] that will always be even,
* allowing it to be sub-divided.
*/
private fun Dimension.mosaicSize(): Int { private fun Dimension.mosaicSize(): Int {
val size = pxOrElse { 512 } val size = pxOrElse { 512 }
return if (size.mod(2) > 0) size + 1 else size return if (size.mod(2) > 0) size + 1 else size

View file

@ -50,9 +50,7 @@ class SquareFrameTransform : Transformation {
} }
companion object { companion object {
/** /** A re-usable instance. */
* A shared instance that can be re-used.
*/
val INSTANCE = SquareFrameTransform() val INSTANCE = SquareFrameTransform()
} }
} }

View file

@ -112,7 +112,7 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Extende
* closed when the view is destroyed. If a menu is already opened, this call is ignored. * closed when the view is destroyed. If a menu is already opened, this call is ignored.
* @param anchor The [View] to anchor the menu to. * @param anchor The [View] to anchor the menu to.
* @param menuRes The resource of the menu to load. * @param menuRes The resource of the menu to load.
* @param song The [Artist] to create the menu for. * @param album The [Album] to create the menu for.
*/ */
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) { protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
logD("Launching new album menu: ${album.rawName}") logD("Launching new album menu: ${album.rawName}")
@ -148,7 +148,7 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Extende
* closed when the view is destroyed. If a menu is already opened, this call is ignored. * closed when the view is destroyed. If a menu is already opened, this call is ignored.
* @param anchor The [View] to anchor the menu to. * @param anchor The [View] to anchor the menu to.
* @param menuRes The resource of the menu to load. * @param menuRes The resource of the menu to load.
* @param song The [Artist] to create the menu for. * @param artist The [Artist] to create the menu for.
*/ */
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) { protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
logD("Launching new artist menu: ${artist.rawName}") logD("Launching new artist menu: ${artist.rawName}")
@ -181,7 +181,7 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Extende
* closed when the view is destroyed. If a menu is already opened, this call is ignored. * closed when the view is destroyed. If a menu is already opened, this call is ignored.
* @param anchor The [View] to anchor the menu to. * @param anchor The [View] to anchor the menu to.
* @param menuRes The resource of the menu to load. * @param menuRes The resource of the menu to load.
* @param song The [Genre] to create the menu for. * @param genre The [Genre] to create the menu for.
*/ */
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) { protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
logD("Launching new genre menu: ${genre.rawName}") logD("Launching new genre menu: ${genre.rawName}")
@ -209,12 +209,6 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Extende
} }
} }
/**
* Internally create a menu for a [Music] item.
* @param anchor The [View] to anchor the menu to.
* @param menuRes The resource of the menu to load.
* @param onMenuItemClick A callback for when a [MenuItem] is selected.
*/
private fun openMusicMenuImpl( private fun openMusicMenuImpl(
anchor: View, anchor: View,
@MenuRes menuRes: Int, @MenuRes menuRes: Int,

View file

@ -38,19 +38,6 @@ open class AuxioRecyclerView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
RecyclerView(context, attrs, defStyleAttr) { RecyclerView(context, attrs, defStyleAttr) {
/**
* An adapter-specific hook to [GridLayoutManager.SpanSizeLookup].
*/
interface SpanSizeLookup {
/**
* Get if the item at a position takes up the whole width of the [RecyclerView] or not.
* @param position The position of the item.
* @return true if the item is full-width, false otherwise.
*/
fun isItemFullWidth(position: Int): Boolean
}
// Keep track of the layout-defined bottom padding so we can re-apply it when applying insets.
private val initialPaddingBottom = paddingBottom private val initialPaddingBottom = paddingBottom
init { init {
@ -92,4 +79,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
} }
} }
/** An [RecyclerView.Adapter]-specific hook to [GridLayoutManager.SpanSizeLookup]. */
interface SpanSizeLookup {
/**
* Get if the item at a position takes up the whole width of the [RecyclerView] or not.
* @param position The position of the item.
* @return true if the item is full-width, false otherwise.
*/
fun isItemFullWidth(position: Int): Boolean
}
} }

View file

@ -40,19 +40,6 @@ class DialogRecyclerView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
RecyclerView(context, attrs, defStyleAttr) { RecyclerView(context, attrs, defStyleAttr) {
/**
* A [RecyclerView.ViewHolder] that implements dialog-specific fixes.
*/
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
init {
// ViewHolders are not automatically full-width in dialogs, manually resize
// them to be as such.
root.layoutParams =
LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
}
}
private val topDivider = MaterialDivider(context) private val topDivider = MaterialDivider(context)
private val bottomDivider = MaterialDivider(context) private val bottomDivider = MaterialDivider(context)
private val spacingMedium = context.getDimenPixels(R.dimen.spacing_medium) private val spacingMedium = context.getDimenPixels(R.dimen.spacing_medium)
@ -90,10 +77,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
invalidateDividers() invalidateDividers()
} }
/**
* Measure a [divider] with the equivalent of match_parent and wrap_content.
* @param divider The divider to measure.
*/
private fun measureDivider(divider: MaterialDivider) { private fun measureDivider(divider: MaterialDivider) {
val widthMeasureSpec = val widthMeasureSpec =
ViewGroup.getChildMeasureSpec( ViewGroup.getChildMeasureSpec(
@ -108,9 +91,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
divider.measure(widthMeasureSpec, heightMeasureSpec) divider.measure(widthMeasureSpec, heightMeasureSpec)
} }
/**
* Invalidate the visibility of both dividers.
*/
private fun invalidateDividers() { private fun invalidateDividers() {
val lmm = layoutManager as LinearLayoutManager val lmm = layoutManager as LinearLayoutManager
// Top divider should only be visible when the first item has gone off-screen. // Top divider should only be visible when the first item has gone off-screen.
@ -119,5 +99,18 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
bottomDivider.isInvisible = bottomDivider.isInvisible =
lmm.findLastCompletelyVisibleItemPosition() == (lmm.itemCount - 1) lmm.findLastCompletelyVisibleItemPosition() == (lmm.itemCount - 1)
} }
/**
* A [RecyclerView.ViewHolder] that implements dialog-specific fixes.
*/
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
init {
// ViewHolders are not automatically full-width in dialogs, manually resize
// them to be as such.
root.layoutParams =
LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
}
}
} }

View file

@ -28,24 +28,10 @@ import org.oxycblt.auxio.util.logW
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() { abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
/**
* A [RecyclerView.ViewHolder] that can display a playing indicator.
*/
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
/**
* Update the playing indicator within this [RecyclerView.ViewHolder].
* @param isActive True if this item is playing, false otherwise.
* @param isPlaying True if playback is ongoing, false if paused. If this
* is true, [isActive] will also be true.
*/
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
}
// There are actually two states for this adapter: // There are actually two states for this adapter:
// This is sub-divided into two states:
// - The currently playing item, which is usually marked as "selected" and becomes accented. // - The currently playing item, which is usually marked as "selected" and becomes accented.
// - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is // - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is
// displaying // marked as "playing" or not.
private var currentItem: Item? = null private var currentItem: Item? = null
private var isPlaying = false private var isPlaying = false
@ -119,6 +105,19 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
} }
} }
/**
* A [RecyclerView.ViewHolder] that can display a playing indicator.
*/
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
/**
* Update the playing indicator within this [RecyclerView.ViewHolder].
* @param isActive True if this item is playing, false otherwise.
* @param isPlaying True if playback is ongoing, false if paused. If this
* is true, [isActive] will also be true.
*/
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
}
companion object { companion object {
private val PAYLOAD_PLAYING_INDICATOR_CHANGED = Any() private val PAYLOAD_PLAYING_INDICATOR_CHANGED = Any()
} }

View file

@ -28,17 +28,6 @@ import org.oxycblt.auxio.music.Music
*/ */
abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> : abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
PlayingIndicatorAdapter<VH>() { PlayingIndicatorAdapter<VH>() {
/**
* A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator.
*/
abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) {
/**
* Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder].
* @param isSelected Whether this [PlayingIndicatorAdapter.ViewHolder] is selected.
*/
abstract fun updateSelectionIndicator(isSelected: Boolean)
}
private var selectedItems = setOf<Music>() private var selectedItems = setOf<Music>()
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) { override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
@ -79,6 +68,17 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
} }
} }
/**
* A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator.
*/
abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) {
/**
* Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder].
* @param isSelected Whether this [PlayingIndicatorAdapter.ViewHolder] is selected.
*/
abstract fun updateSelectionIndicator(isSelected: Boolean)
}
companion object { companion object {
private val PAYLOAD_SELECTION_INDICATOR_CHANGED = Any() private val PAYLOAD_SELECTION_INDICATOR_CHANGED = Any()
} }

View file

@ -113,6 +113,7 @@ class SyncListDiffer<T>(
/** /**
* Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only * Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only
* use it if the changes are trivial. * use it if the changes are trivial.
* @param newList The list to update to.
*/ */
fun submitList(newList: List<T>) { fun submitList(newList: List<T>) {
if (newList == currentList) { if (newList == currentList) {
@ -126,6 +127,7 @@ class SyncListDiffer<T>(
/** /**
* Replace this list with a new list. This is good for large diffs that are too slow to * Replace this list with a new list. This is good for large diffs that are too slow to
* update synchronously, but too chaotic to update asynchronously. * update synchronously, but too chaotic to update asynchronously.
* @param newList The list to update to.
*/ */
fun replaceList(newList: List<T>) { fun replaceList(newList: List<T>) {
if (newList == currentList) { if (newList == currentList) {

View file

@ -35,7 +35,7 @@ import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
* A basic [RecyclerView.ViewHolder] that displays a [Song]. Use [new] to create an instance. * A [RecyclerView.ViewHolder] that displays a [Song]. Use [new] to create an instance.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SongViewHolder private constructor(private val binding: ItemSongBinding) : class SongViewHolder private constructor(private val binding: ItemSongBinding) :
@ -82,7 +82,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
} }
/** /**
* A basic [RecyclerView.ViewHolder] that displays a [Album]. Use [new] to create an instance. * A [RecyclerView.ViewHolder] that displays a [Album]. Use [new] to create an instance.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AlbumViewHolder private constructor(private val binding: ItemParentBinding) : class AlbumViewHolder private constructor(private val binding: ItemParentBinding) :
@ -131,7 +131,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
} }
/** /**
* A basic [RecyclerView.ViewHolder] that displays a [Artist]. Use [new] to create an instance. * A [RecyclerView.ViewHolder] that displays a [Artist]. Use [new] to create an instance.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class ArtistViewHolder private constructor(private val binding: ItemParentBinding) : class ArtistViewHolder private constructor(private val binding: ItemParentBinding) :
@ -189,7 +189,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
} }
/** /**
* A basic [RecyclerView.ViewHolder] that displays a [Genre]. Use [new] to create an instance. * A [RecyclerView.ViewHolder] that displays a [Genre]. Use [new] to create an instance.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class GenreViewHolder private constructor(private val binding: ItemParentBinding) : class GenreViewHolder private constructor(private val binding: ItemParentBinding) :
@ -240,7 +240,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
} }
/** /**
* A basic [RecyclerView.ViewHolder] that displays a [Header]. Use [new] to create an instance. * A [RecyclerView.ViewHolder] that displays a [Header]. Use [new] to create an instance.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) : class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :

View file

@ -38,9 +38,7 @@ class SelectionToolbarOverlay
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) { FrameLayout(context, attrs, defStyleAttr) {
// This will be populated after the inflation completes.
private lateinit var innerToolbar: MaterialToolbar private lateinit var innerToolbar: MaterialToolbar
// The selection toolbar will be overlaid over the inner toolbar when shown.
private val selectionToolbar = private val selectionToolbar =
MaterialToolbar(context).apply { MaterialToolbar(context).apply {
setNavigationIcon(R.drawable.ic_close_24) setNavigationIcon(R.drawable.ic_close_24)
@ -50,7 +48,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
isInvisible = true isInvisible = true
} }
} }
// Animator to handle selection visibility animations
private var fadeThroughAnimator: ValueAnimator? = null private var fadeThroughAnimator: ValueAnimator? = null
override fun onFinishInflate() { override fun onFinishInflate() {
@ -61,7 +58,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
// The inner toolbar should be the first child. // The inner toolbar should be the first child.
innerToolbar = getChildAt(0) as MaterialToolbar innerToolbar = getChildAt(0) as MaterialToolbar
// Now layer the selection toolbar on top. // Selection toolbar should appear on top of the inner toolbar.
addView(selectionToolbar) addView(selectionToolbar)
} }
@ -69,6 +66,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
* Set an OnClickListener for when the "cancel" button in the selection [MaterialToolbar] is * Set an OnClickListener for when the "cancel" button in the selection [MaterialToolbar] is
* pressed. * pressed.
* @param listener The OnClickListener to respond to this interaction. * @param listener The OnClickListener to respond to this interaction.
* @see MaterialToolbar.setNavigationOnClickListener
*/ */
fun setOnSelectionCancelListener(listener: OnClickListener) { fun setOnSelectionCancelListener(listener: OnClickListener) {
selectionToolbar.setNavigationOnClickListener(listener) selectionToolbar.setNavigationOnClickListener(listener)
@ -78,6 +76,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
* Set an [OnMenuItemClickListener] for when a MenuItem is selected from the selection * Set an [OnMenuItemClickListener] for when a MenuItem is selected from the selection
* [MaterialToolbar]. * [MaterialToolbar].
* @param listener The [OnMenuItemClickListener] to respond to this interaction. * @param listener The [OnMenuItemClickListener] to respond to this interaction.
* @see MaterialToolbar.setOnMenuItemClickListener
*/ */
fun setOnMenuItemClickListener(listener: OnMenuItemClickListener?) { fun setOnMenuItemClickListener(listener: OnMenuItemClickListener?) {
selectionToolbar.setOnMenuItemClickListener(listener) selectionToolbar.setOnMenuItemClickListener(listener)
@ -134,7 +133,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (!isLaidOut) { if (!isLaidOut) {
// Not laid out, just change it immediately while are not shown to the user. // Not laid out, just change it immediately while are not shown to the user.
// This is an initialization, so we return false despite changing. // This is an initialization, so we return false despite changing.
changeToolbarAlpha(targetInnerAlpha) setToolbarsAlpha(targetInnerAlpha)
return false return false
} }
@ -146,7 +145,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
fadeThroughAnimator = fadeThroughAnimator =
ValueAnimator.ofFloat(innerToolbar.alpha, targetInnerAlpha).apply { ValueAnimator.ofFloat(innerToolbar.alpha, targetInnerAlpha).apply {
duration = targetDuration duration = targetDuration
addUpdateListener { changeToolbarAlpha(it.animatedValue as Float) } addUpdateListener { setToolbarsAlpha(it.animatedValue as Float) }
start() start()
} }
@ -158,7 +157,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
* @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the * @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the
* inverse opacity of the selection [MaterialToolbar]. * inverse opacity of the selection [MaterialToolbar].
*/ */
private fun changeToolbarAlpha(innerAlpha: Float) { private fun setToolbarsAlpha(innerAlpha: Float) {
innerToolbar.apply { innerToolbar.apply {
alpha = innerAlpha alpha = innerAlpha
isInvisible = innerAlpha == 0f isInvisible = innerAlpha == 0f

View file

@ -167,20 +167,13 @@ sealed class Music : Item {
override fun toString() = "${format.namespace}:${mode.intCode.toString(16)}-$uuid" override fun toString() = "${format.namespace}:${mode.intCode.toString(16)}-$uuid"
/** /**
* Defines the format of this [UID]. * Internal marker of [Music.UID] format type.
* @param namespace The namespace that will be used in the [UID]'s string representation * @param namespace Namespace to use in the [Music.UID]'s string representation.
* to indicate the format.
*/ */
private enum class Format(val namespace: String) { private enum class Format(val namespace: String) {
/** /** @see auxio */
* Auxio-style [UID]s derived from hash of the*non-subjective, unlikely-to-change
* metadata.
*/
AUXIO("org.oxycblt.auxio"), AUXIO("org.oxycblt.auxio"),
/** @see musicBrainz */
/**
* Auxio-style [UID]s derived from a MusicBrainz ID.
*/
MUSICBRAINZ("org.musicbrainz") MUSICBRAINZ("org.musicbrainz")
} }
@ -250,9 +243,7 @@ sealed class Music : Item {
} }
companion object { companion object {
/** /** Cached collator instance re-used with [makeCollationKeyImpl]. */
* Cached collator instance to be used with [makeCollationKeyImpl].
*/
private val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY } private val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY }
} }
} }
@ -308,23 +299,21 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
override val collationKey = makeCollationKeyImpl() override val collationKey = makeCollationKeyImpl()
override fun resolveName(context: Context) = rawName override fun resolveName(context: Context) = rawName
/** /** The track number. Will be null if no valid track number was present in the metadata. */
* The track number. Will be null if no valid track number was present in the metadata.
*/
val track = raw.track val track = raw.track
/**
* The disc number. Will be null if no valid disc number was present in the metadata. /** The disc number. Will be null if no valid disc number was present in the metadata. */
*/
val disc = raw.disc val disc = raw.disc
/**
* The release [Date]. Will be null if no valid date was present in the metadata. /** The release [Date]. Will be null if no valid date was present in the metadata. */
*/
val date = raw.date val date = raw.date
/** /**
* The URI to the audio file that this instance was created from. This can be used to * The URI to the audio file that this instance was created from. This can be used to
* access the audio file in a way that is scoped-storage-safe. * access the audio file in a way that is scoped-storage-safe.
*/ */
val uri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri() val uri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()
/** /**
* The [Path] to this audio file. This is only intended for display, [uri] should be * The [Path] to this audio file. This is only intended for display, [uri] should be
* favored instead for accessing the audio file. * favored instead for accessing the audio file.
@ -333,24 +322,20 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
Path( Path(
name = requireNotNull(raw.fileName) { "Invalid raw: No display name" }, name = requireNotNull(raw.fileName) { "Invalid raw: No display name" },
parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" }) parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" })
/**
* The [MimeType] of the audio file. Only intended for display. /** The [MimeType] of the audio file. Only intended for display. */
*/
val mimeType = val mimeType =
MimeType( MimeType(
fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" }, fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" },
fromFormat = raw.formatMimeType) fromFormat = raw.formatMimeType)
/**
* The size of the audio file, in bytes. /** The size of the audio file, in bytes. */
*/
val size = requireNotNull(raw.size) { "Invalid raw: No size" } val size = requireNotNull(raw.size) { "Invalid raw: No size" }
/**
* The duration of the audio file, in milliseconds. /** The duration of the audio file, in milliseconds. */
*/
val durationMs = requireNotNull(raw.durationMs) { "Invalid raw: No duration" } val durationMs = requireNotNull(raw.durationMs) { "Invalid raw: No duration" }
/**
* The date the audio file was added to the device, as a unix epoch timestamp. /** The date the audio file was added to the device, as a unix epoch timestamp. */
*/
val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" } val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" }
private var _album: Album? = null private var _album: Album? = null
@ -532,109 +517,57 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
* ID is highly unstable and should only be used for accessing the audio file. * ID is highly unstable and should only be used for accessing the audio file.
*/ */
var mediaStoreId: Long? = null, var mediaStoreId: Long? = null,
/** /** @see Song.dateAdded */
* @see Song.dateAdded
*/
var dateAdded: Long? = null, var dateAdded: Long? = null,
/** /** The latest date the [Song]'s audio file was modified, as a unix epoch timestamp. */
* The latest date the [Song]'s audio file was modified, as a unix epoch timestamp.
*/
var dateModified: Long? = null, var dateModified: Long? = null,
/** /** @see Song.path */
* @see Song.path
*/
var fileName: String? = null, var fileName: String? = null,
/** /** @see Song.path */
* @see Song.path
*/
var directory: Directory? = null, var directory: Directory? = null,
/** /** @see Song.size */
* @see Song.size
*/
var size: Long? = null, var size: Long? = null,
/** /** @see Song.durationMs */
* @see Song.durationMs
*/
var durationMs: Long? = null, var durationMs: Long? = null,
/** /** @see Song.mimeType */
* @see Song.mimeType
*/
var extensionMimeType: String? = null, var extensionMimeType: String? = null,
/** /** @see Song.mimeType */
* @see Song.mimeType
*/
var formatMimeType: String? = null, var formatMimeType: String? = null,
/** /** @see Music.UID */
* @see Music.UID
*/
var musicBrainzId: String? = null, var musicBrainzId: String? = null,
/** /** @see Music.rawName */
* @see Music.rawName
*/
var name: String? = null, var name: String? = null,
/** /** @see Music.rawSortName */
* @see Music.rawSortName
*/
var sortName: String? = null, var sortName: String? = null,
/** /** @see Song.track */
* @see Song.track
*/
var track: Int? = null, var track: Int? = null,
/** /** @see Song.disc */
* @see Song.disc
*/
var disc: Int? = null, var disc: Int? = null,
/** /** @see Song.date */
* @see Song.date
*/
var date: Date? = null, var date: Date? = null,
/** /** @see Album.Raw.mediaStoreId */
* @see Album.Raw.mediaStoreId
*/
var albumMediaStoreId: Long? = null, var albumMediaStoreId: Long? = null,
/** /** @see Album.Raw.musicBrainzId */
* @see Album.Raw.musicBrainzId
*/
var albumMusicBrainzId: String? = null, var albumMusicBrainzId: String? = null,
/** /** @see Album.Raw.name */
* @see Album.Raw.name
*/
var albumName: String? = null, var albumName: String? = null,
/** /** @see Album.Raw.sortName */
* @see Album.Raw.sortName
*/
var albumSortName: String? = null, var albumSortName: String? = null,
/** /** @see Album.Raw.type */
* @see Album.Raw.type
*/
var albumTypes: List<String> = listOf(), var albumTypes: List<String> = listOf(),
/** /** @see Artist.Raw.musicBrainzId */
* @see Artist.Raw.musicBrainzId
*/
var artistMusicBrainzIds: List<String> = listOf(), var artistMusicBrainzIds: List<String> = listOf(),
/** /** @see Artist.Raw.name */
* @see Artist.Raw.name
*/
var artistNames: List<String> = listOf(), var artistNames: List<String> = listOf(),
/** /** @see Artist.Raw.sortName */
* @see Artist.Raw.sortName
*/
var artistSortNames: List<String> = listOf(), var artistSortNames: List<String> = listOf(),
/** /** @see Artist.Raw.musicBrainzId */
* @see Artist.Raw.musicBrainzId
*/
var albumArtistMusicBrainzIds: List<String> = listOf(), var albumArtistMusicBrainzIds: List<String> = listOf(),
/** /** @see Artist.Raw.name */
* @see Artist.Raw.name
*/
var albumArtistNames: List<String> = listOf(), var albumArtistNames: List<String> = listOf(),
/** /** @see Artist.Raw.sortName */
* @see Artist.Raw.sortName
*/
var albumArtistSortNames: List<String> = listOf(), var albumArtistSortNames: List<String> = listOf(),
/** /** @see Genre.Raw.name */
* @see Genre.Raw
*/
var genreNames: List<String> = listOf() var genreNames: List<String> = listOf()
) )
} }
@ -669,23 +602,24 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
* TODO: Date ranges? * TODO: Date ranges?
*/ */
val date: Date? val date: Date?
/** /**
* The [Type] of this album, signifying the type of release it actually is. * The [Type] of this album, signifying the type of release it actually is.
* Defaults to [Type.Album]. * Defaults to [Type.Album].
*/ */
val type = raw.type ?: Type.Album(null) val type = raw.type ?: Type.Album(null)
/** /**
* The URI to a MediaStore-provided album cover. These images will be fast to load, but * The URI to a MediaStore-provided album cover. These images will be fast to load, but
* at the cost of image quality. * at the cost of image quality.
*/ */
val coverUri = raw.mediaStoreId.toCoverUri() val coverUri = raw.mediaStoreId.toCoverUri()
/**
* The duration of all songs in the album, in milliseconds. /** The duration of all songs in the album, in milliseconds. */
*/
val durationMs: Long val durationMs: Long
/**
* The earliest date a song in this album was added, as a unix epoch timestamp. /** The earliest date a song in this album was added, as a unix epoch timestamp. */
*/
val dateAdded: Long val dateAdded: Long
init { init {
@ -798,9 +732,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
*/ */
abstract val refinement: Refinement? abstract val refinement: Refinement?
/** /** The string resource corresponding to the name of this release type to show in the UI. */
* The string resource corresponding to the name of this release type to show in the UI.
*/
abstract val stringRes: Int abstract val stringRes: Int
/** /**
@ -999,34 +931,28 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
* cover art. * cover art.
*/ */
val mediaStoreId: Long, val mediaStoreId: Long,
/** /** @see Music.uid */
* @see Music.uid
*/
val musicBrainzId: UUID?, val musicBrainzId: UUID?,
/** /** @see Music.rawName */
* @see Music.rawName
*/
val name: String, val name: String,
/** /** @see Music.rawSortName */
* @see Music.rawSortName
*/
val sortName: String?, val sortName: String?,
/** /** @see Album.type */
* @see Album.type
*/
val type: Type?, val type: Type?,
/** /** @see Artist.Raw.name */
* @see Artist.Raw.name
*/
val rawArtists: List<Artist.Raw> val rawArtists: List<Artist.Raw>
) { ) {
// Albums are grouped as follows:
// - If we have a MusicBrainz ID, only group by it. This allows different Albums with the
// same name to be differentiated, which is common in large libraries.
// - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase
// artist name. This allows for case-insensitive artist/album grouping, which can be common
// for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein").
// Cache the hash-code for HashMap efficiency. // Cache the hash-code for HashMap efficiency.
private val hashCode = private val hashCode =
musicBrainzId?.hashCode() ?: (31 * name.lowercase().hashCode() + rawArtists.hashCode()) musicBrainzId?.hashCode() ?: (31 * name.lowercase().hashCode() + rawArtists.hashCode())
// Make Album.Raw equality based on album name and raw artist lists in order to
// differentiate between albums with the same name but different artists.
override fun hashCode() = hashCode override fun hashCode() = hashCode
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -1068,11 +994,13 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
* thus included in this list. * thus included in this list.
*/ */
val albums: List<Album> val albums: List<Album>
/** /**
* The duration of all [Song]s in the artist, in milliseconds. * The duration of all [Song]s in the artist, in milliseconds.
* Will be null if there are no songs. * Will be null if there are no songs.
*/ */
val durationMs: Long? val durationMs: Long?
/** /**
* Whether this artist is considered a "collaborator", i.e it is not directly credited on * Whether this artist is considered a "collaborator", i.e it is not directly credited on
* any [Album]. * any [Album].
@ -1159,19 +1087,19 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
* **This is only meant for use within the music package.** * **This is only meant for use within the music package.**
*/ */
class Raw( class Raw(
/** /** @see Music.UID */
* @see Music.UID
*/
val musicBrainzId: UUID? = null, val musicBrainzId: UUID? = null,
/** /** @see Music.rawName */
* @see Music.rawName
*/
val name: String? = null, val name: String? = null,
/** /** @see Music.rawSortName */
* @see Music.rawSortName
*/
val sortName: String? = null val sortName: String? = null
) { ) {
// Artists are grouped as follows:
// - If we have a MusicBrainz ID, only group by it. This allows different Artists with the
// same name to be differentiated, which is common in large libraries.
// - If we do not have a MusicBrainz ID, compare by the lowercase name. This allows artist
// grouping to be case-insensitive.
// Cache the hashCode for HashMap efficiency. // Cache the hashCode for HashMap efficiency.
private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode() private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode()
@ -1271,6 +1199,10 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
*/ */
val name: String? = null val name: String? = null
) { ) {
// Only group by the lowercase genre name. This allows Genre grouping to be
// case-insensitive, which may be helpful in some libraries with different ways of
// formatting genres.
// Cache the hashCode for HashMap efficiency. // Cache the hashCode for HashMap efficiency.
private val hashCode = name?.lowercase().hashCode() private val hashCode = name?.lowercase().hashCode()
@ -1361,22 +1293,16 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
private fun StringBuilder.appendDate(): StringBuilder { private fun StringBuilder.appendDate(): StringBuilder {
// Construct an ISO-8601 date, dropping precision that doesn't exist. // Construct an ISO-8601 date, dropping precision that doesn't exist.
append(year.toFixedString(4)) append(year.toStringFixed(4))
append("-${(month ?: return this).toFixedString(2)}") append("-${(month ?: return this).toStringFixed(2)}")
append("-${(day ?: return this).toFixedString(2)}") append("-${(day ?: return this).toStringFixed(2)}")
append("T${(hour ?: return this).toFixedString(2)}") append("T${(hour ?: return this).toStringFixed(2)}")
append(":${(minute ?: return this.append('Z')).toFixedString(2)}") append(":${(minute ?: return this.append('Z')).toStringFixed(2)}")
append(":${(second ?: return this.append('Z')).toFixedString(2)}") append(":${(second ?: return this.append('Z')).toStringFixed(2)}")
return this.append('Z') return this.append('Z')
} }
/** private fun Int.toStringFixed(len: Int) = toString().padStart(len, '0').substring(0 until len)
* Converts an integer to a fixed-size [String] of the specified length.
* @param len The end length of the formatted [String].
* @return The integer as a formatted [String] prefixed with zeroes in order to make it
* the specified length.
*/
private fun Int.toFixedString(len: Int) = toString().padStart(len, '0').substring(0 until len)
companion object { companion object {
/** /**

View file

@ -24,24 +24,13 @@ import org.oxycblt.auxio.IntegerTable
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
enum class MusicMode { enum class MusicMode {
/** /** Configure with respect to [Song] instances. */
* Configure with respect to [Song] instances.
*/
SONGS, SONGS,
/** Configure with respect to [Album] instances. */
/**
* Configure with respect to [Album] instances.
*/
ALBUMS, ALBUMS,
/** Configure with respect to [Artist] instances. */
/**
* Configure with respect to [Artist] instances.
*/
ARTISTS, ARTISTS,
/** Configure with respect to [Genre] instances. */
/**
* Configure with respect to [Genre] instances.
*/
GENRES; GENRES;
/** /**

View file

@ -20,8 +20,6 @@ package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import org.oxycblt.auxio.music.MusicStore.Callback
import org.oxycblt.auxio.music.MusicStore.Library
import org.oxycblt.auxio.music.storage.useQuery import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.music.storage.contentResolverSafe
@ -92,8 +90,8 @@ class MusicStore private constructor() {
init { init {
// The data passed to Library initially are complete, but are still volitaile. // The data passed to Library initially are complete, but are still volitaile.
// Finalize them to ensure they are well-formed. Initialize the UID map in the // Finalize them to ensure they are well-formed. Also initialize the UID map in
// same loop for efficiency. // the same loop for efficiency.
for (song in songs) { for (song in songs) {
song._finalize() song._finalize()
uidMap[song.uid] = song uidMap[song.uid] = song
@ -146,7 +144,7 @@ class MusicStore private constructor() {
/** /**
* Convert a [Genre] from an another library into a [Genre] in this [Library]. * Convert a [Genre] from an another library into a [Genre] in this [Library].
* @param song The [Genre] to convert. * @param genre The [Genre] to convert.
* @return The analogous [Genre] in this [Library], or null if it does not exist. * @return The analogous [Genre] in this [Library], or null if it does not exist.
*/ */
fun sanitize(genre: Genre) = find<Genre>(genre.uid) fun sanitize(genre: Genre) = find<Genre>(genre.uid)
@ -170,9 +168,7 @@ class MusicStore private constructor() {
} }
} }
/** /** A callback for changes in the music library. */
* A callback for changes in the music library.
*/
interface Callback { interface Callback {
/** /**
* Called when the current [Library] has changed. * Called when the current [Library] has changed.

View file

@ -31,17 +31,11 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
private val indexer = Indexer.getInstance() private val indexer = Indexer.getInstance()
private val _indexerState = MutableStateFlow<Indexer.State?>(null) private val _indexerState = MutableStateFlow<Indexer.State?>(null)
/** /** The current music loading state, or null if no loading is going on. */
* The current music loading state, or null if no loading is going on.
* @see Indexer.State
*/
val indexerState: StateFlow<Indexer.State?> = _indexerState val indexerState: StateFlow<Indexer.State?> = _indexerState
private val _statistics = MutableStateFlow<Statistics?>(null) private val _statistics = MutableStateFlow<Statistics?>(null)
/** /** [Statistics] about the last completed music load. */
* Statistics about the last completed music load.
* @see Statistics
*/
val statistics: StateFlow<Statistics?> val statistics: StateFlow<Statistics?>
get() = _statistics get() = _statistics
@ -68,16 +62,12 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
} }
} }
/** /** Requests that the music library should be re-loaded while leveraging the cache. */
* Requests that the music library should be re-loaded while leveraging the cache.
*/
fun refresh() { fun refresh() {
indexer.requestReindex(true) indexer.requestReindex(true)
} }
/** /** Requests that the music library be re-loaded without the cache. */
* Requests that the music library should be re-loaded while ignoring the cache.
*/
fun rescan() { fun rescan() {
indexer.requestReindex(false) indexer.requestReindex(false)
} }

View file

@ -129,19 +129,14 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
*/ */
val intCode: Int val intCode: Int
// Sort's integer representation is formatted as AMMMM, where A is a bitflag // Sort's integer representation is formatted as AMMMM, where A is a bitflag
// representing on if the mode is ascending or descending, and M is the integer // representing if the sort is in ascending or descending order, and M is the
// representation of the sort mode. // integer representation of the sort mode.
get() = mode.intCode.shl(1) or if (isAscending) 1 else 0 get() = mode.intCode.shl(1) or if (isAscending) 1 else 0
sealed class Mode { sealed class Mode {
/** /** The integer representation of this sort mode. */
* The integer representation of this sort mode.
*/
abstract val intCode: Int abstract val intCode: Int
/** The item ID of this sort mode in menu resources. */
/**
* The item ID of this sort mode in menu resources.
*/
abstract val itemId: Int abstract val itemId: Int
/** /**
@ -276,9 +271,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareBy(BasicComparator.ALBUM)) compareBy(BasicComparator.ALBUM))
} }
/** /** Sort by the duration of an item. */
* Sort by the duration of an item.
*/
object ByDuration : Mode() { object ByDuration : Mode() {
override val intCode: Int override val intCode: Int
get() = IntegerTable.SORT_BY_DURATION get() = IntegerTable.SORT_BY_DURATION
@ -494,9 +487,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
} }
companion object { companion object {
/** /** A re-usable configured for [Artist]s.. */
* A shared instance configured for [Artist]s that can be re-used.
*/
val ARTISTS: Comparator<List<Artist>> = ListComparator(BasicComparator.ARTIST) val ARTISTS: Comparator<List<Artist>> = ListComparator(BasicComparator.ARTIST)
} }
} }
@ -520,21 +511,13 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
} }
companion object { companion object {
/** /** A re-usable instance configured for [Song]s. */
* A shared instance configured for [Song]s that can be re-used.
*/
val SONG: Comparator<Song> = BasicComparator() val SONG: Comparator<Song> = BasicComparator()
/** /** A re-usable instance configured for [Album]s. */
* A shared instance configured for [Album]s that can be re-used.
*/
val ALBUM: Comparator<Album> = BasicComparator() val ALBUM: Comparator<Album> = BasicComparator()
/** /** A re-usable instance configured for [Artist]s. */
* A shared instance configured for [Artist]s that can be re-used.
*/
val ARTIST: Comparator<Artist> = BasicComparator() val ARTIST: Comparator<Artist> = BasicComparator()
/** /** A re-usable instance configured for [Genre]s. */
* A shared instance configured for [Genre]s that can be re-used.
*/
val GENRE: Comparator<Genre> = BasicComparator() val GENRE: Comparator<Genre> = BasicComparator()
} }
} }
@ -553,17 +536,11 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
} }
companion object { companion object {
/** /** A re-usable instance configured for [Int]s. */
* A shared instance configured for [Int]s that can be re-used.
*/
val INT = NullableComparator<Int>() val INT = NullableComparator<Int>()
/** /** A re-usable instance configured for [Long]s. */
* A shared instance configured for [Long]s that can be re-used.
*/
val LONG = NullableComparator<Long>() val LONG = NullableComparator<Long>()
/** /** A re-usable instance configured for [Date]s. */
* A shared instance configured for [Date]s that can be re-used.
*/
val DATE = NullableComparator<Date>() val DATE = NullableComparator<Date>()
} }
} }

View file

@ -44,7 +44,7 @@ interface CacheExtractor {
fun init() fun init()
/** /**
* Finalize the Extractor by writing the newly-loaded [Song.Raw] back into the cache, * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
* alongside freeing up memory. * alongside freeing up memory.
* @param rawSongs The songs to write into the cache. * @param rawSongs The songs to write into the cache.
*/ */
@ -437,123 +437,58 @@ private class CacheDatabase(context: Context) :
* Defines the columns used in this database. * Defines the columns used in this database.
*/ */
private object Columns { private object Columns {
/** /** @see Song.Raw.mediaStoreId */
* @see Song.Raw.mediaStoreId
*/
const val MEDIA_STORE_ID = "msid" const val MEDIA_STORE_ID = "msid"
/** /** @see Song.Raw.dateAdded */
* @see Song.Raw.dateAdded
*/
const val DATE_ADDED = "date_added" const val DATE_ADDED = "date_added"
/** /** @see Song.Raw.dateModified */
* @see Song.Raw.dateModified
*/
const val DATE_MODIFIED = "date_modified" const val DATE_MODIFIED = "date_modified"
/** @see Song.Raw.size */
/**
* @see Song.Raw.size
*/
const val SIZE = "size" const val SIZE = "size"
/** /** @see Song.Raw.durationMs */
* @see Song.Raw.durationMs
*/
const val DURATION = "duration" const val DURATION = "duration"
/** /** @see Song.Raw.formatMimeType */
* @see Song.Raw.formatMimeType
*/
const val FORMAT_MIME_TYPE = "fmt_mime" const val FORMAT_MIME_TYPE = "fmt_mime"
/** @see Song.Raw.musicBrainzId */
/**
* @see Song.Raw.musicBrainzId
*/
const val MUSIC_BRAINZ_ID = "mbid" const val MUSIC_BRAINZ_ID = "mbid"
/** /** @see Song.Raw.name */
* @see Song.Raw.name
*/
const val NAME = "name" const val NAME = "name"
/** /** @see Song.Raw.sortName */
* @see Song.Raw.sortName
*/
const val SORT_NAME = "sort_name" const val SORT_NAME = "sort_name"
/** @see Song.Raw.track */
/**
* @see Song.Raw.track
*/
const val TRACK = "track" const val TRACK = "track"
/** /** @see Song.Raw.disc */
* @see Song.Raw.disc
*/
const val DISC = "disc" const val DISC = "disc"
/** /** @see Song.Raw.date */
* @see [Song.Raw.date
*/
const val DATE = "date" const val DATE = "date"
/** @see Song.Raw.albumMusicBrainzId */
/**
* @see [Song.Raw.albumMusicBrainzId
*/
const val ALBUM_MUSIC_BRAINZ_ID = "album_mbid" const val ALBUM_MUSIC_BRAINZ_ID = "album_mbid"
/** /** @see Song.Raw.albumName */
* @see Song.Raw.albumName
*/
const val ALBUM_NAME = "album" const val ALBUM_NAME = "album"
/** /** @see Song.Raw.albumSortName */
* @see Song.Raw.albumSortName
*/
const val ALBUM_SORT_NAME = "album_sort" const val ALBUM_SORT_NAME = "album_sort"
/** /** @see Song.Raw.albumTypes */
* @see Song.Raw.albumReleaseTypes
*/
const val ALBUM_TYPES = "album_types" const val ALBUM_TYPES = "album_types"
/** @see Song.Raw.artistMusicBrainzIds */
/**
* @see Song.Raw.artistMusicBrainzIds
*/
const val ARTIST_MUSIC_BRAINZ_IDS = "artists_mbid" const val ARTIST_MUSIC_BRAINZ_IDS = "artists_mbid"
/** /** @see Song.Raw.artistNames */
* @see Song.Raw.artistNames
*/
const val ARTIST_NAMES = "artists" const val ARTIST_NAMES = "artists"
/** /** @see Song.Raw.artistSortNames */
* @see Song.Raw.artistSortNames
*/
const val ARTIST_SORT_NAMES = "artists_sort" const val ARTIST_SORT_NAMES = "artists_sort"
/** @see Song.Raw.albumArtistMusicBrainzIds */
/**
* @see Song.Raw.albumArtistMusicBrainzIds
*/
const val ALBUM_ARTIST_MUSIC_BRAINZ_IDS = "album_artists_mbid" const val ALBUM_ARTIST_MUSIC_BRAINZ_IDS = "album_artists_mbid"
/** /** @see Song.Raw.albumArtistNames */
* @see Song.Raw.albumArtistNames
*/
const val ALBUM_ARTIST_NAMES = "album_artists" const val ALBUM_ARTIST_NAMES = "album_artists"
/** /** @see Song.Raw.albumArtistSortNames */
* @see Song.Raw.albumArtistSortNames
*/
const val ALBUM_ARTIST_SORT_NAMES = "album_artists_sort" const val ALBUM_ARTIST_SORT_NAMES = "album_artists_sort"
/** @see Song.Raw.genreNames */
/**
* @see Song.Raw.genreNames
*/
const val GENRE_NAMES = "genres" const val GENRE_NAMES = "genres"
} }
companion object { companion object {
/** private const val DB_NAME = "auxio_music_cache.db"
* The file name of the database. private const val DB_VERSION = 1
*/ private const val TABLE_RAW_SONGS = "raw_songs"
const val DB_NAME = "auxio_music_cache.db"
/**
* The current version of the database. Increment whenever a breaking change is made
* to the schema. When incremented, the database will be wiped.
*/
const val DB_VERSION = 1
/**
* The table containing the cached [Song.Raw] instances.
*/
const val TABLE_RAW_SONGS = "raw_songs"
@Volatile private var INSTANCE: CacheDatabase? = null @Volatile private var INSTANCE: CacheDatabase? = null

View file

@ -81,14 +81,10 @@ abstract class MediaStoreExtractor(
* @return A [Cursor] of the music data returned from the database. * @return A [Cursor] of the music data returned from the database.
*/ */
open fun init(): Cursor { open fun init(): Cursor {
// Initialize sub-extractors for later use.
cacheExtractor.init()
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
cacheExtractor.init()
val settings = Settings(context) val settings = Settings(context)
val storageManager = context.getSystemServiceCompat(StorageManager::class) val storageManager = context.getSystemServiceCompat(StorageManager::class)
// Set up the volume list for concrete implementations to use.
volumes = storageManager.storageVolumesCompat
val args = mutableListOf<String>() val args = mutableListOf<String>()
var selector = BASE_SELECTOR var selector = BASE_SELECTOR
@ -151,8 +147,6 @@ abstract class MediaStoreExtractor(
artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST) artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST) albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
logD("Assembling genre map")
// Since we can't obtain the genre tag from a song query, we must construct our own // Since we can't obtain the genre tag from a song query, we must construct our own
// equivalent from genre database queries. Theoretically, this isn't needed since // equivalent from genre database queries. Theoretically, this isn't needed since
// MetadataLayer will fill this in for us, but I'd imagine there are some obscure // MetadataLayer will fill this in for us, but I'd imagine there are some obscure
@ -183,18 +177,21 @@ abstract class MediaStoreExtractor(
} }
} }
volumes = storageManager.storageVolumesCompat
logD("Finished initialization in ${System.currentTimeMillis() - start}ms") logD("Finished initialization in ${System.currentTimeMillis() - start}ms")
return cursor return cursor
} }
/** Finalize this instance by closing the cursor and finalizing the cache. */ /**
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
* alongside freeing up memory.
* @param rawSongs The songs to write into the cache.
*/
fun finalize(rawSongs: List<Song.Raw>) { fun finalize(rawSongs: List<Song.Raw>) {
// Free the cursor (and it's resources) // Free the cursor (and it's resources)
cursor?.close() cursor?.close()
cursor = null cursor = null
// Finalize sub-extractors
cacheExtractor.finalize(rawSongs) cacheExtractor.finalize(rawSongs)
} }
@ -502,6 +499,7 @@ open class Api29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtra
override fun init(): Cursor { override fun init(): Cursor {
val cursor = super.init() val cursor = super.init()
// Set up cursor indices for later use.
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
return cursor return cursor
} }
@ -537,6 +535,7 @@ class Api30MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor)
override fun init(): Cursor { override fun init(): Cursor {
val cursor = super.init() val cursor = super.init()
// Set up cursor indices for later use.
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER) discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER)
return cursor return cursor

View file

@ -56,8 +56,9 @@ class MetadataExtractor(
fun init() = mediaStoreExtractor.init().count fun init() = mediaStoreExtractor.init().count
/** /**
* Finalize this extractor with the newly parsed [Song.Raw]. This actually finalizes the * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
* sub-extractors that this instance relies on. * alongside freeing up memory.
* @param rawSongs The songs to write into the cache.
*/ */
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs) fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs)
@ -85,7 +86,6 @@ class MetadataExtractor(
spin@ while (true) { spin@ while (true) {
for (i in taskPool.indices) { for (i in taskPool.indices) {
val task = taskPool[i] val task = taskPool[i]
if (task != null) { if (task != null) {
val finishedRaw = task.get() val finishedRaw = task.get()
if (finishedRaw != null) { if (finishedRaw != null) {
@ -105,7 +105,6 @@ class MetadataExtractor(
// Spin until all of the remaining tasks are complete. // Spin until all of the remaining tasks are complete.
for (i in taskPool.indices) { for (i in taskPool.indices) {
val task = taskPool[i] val task = taskPool[i]
if (task != null) { if (task != null) {
val finishedRaw = task.get() ?: continue@spin val finishedRaw = task.get() ?: continue@spin
emit(finishedRaw) emit(finishedRaw)
@ -118,9 +117,6 @@ class MetadataExtractor(
} }
companion object { companion object {
/**
* The amount of [Task]s this instance can return
*/
private const val TASK_CAPACITY = 8 private const val TASK_CAPACITY = 8
} }
} }
@ -158,7 +154,6 @@ class Task(context: Context, private val raw: Song.Raw) {
logW(e.stackTraceToString()) logW(e.stackTraceToString())
null null
} }
if (format == null) { if (format == null) {
logD("Nothing could be extracted for ${raw.name}") logD("Nothing could be extracted for ${raw.name}")
return raw return raw

View file

@ -110,7 +110,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String>
} }
if (currentString.isNotEmpty()) { if (currentString.isNotEmpty()) {
// Had an in-progress split string we should add. // Had an in-progress split string that is now terminated, add it..
split.add(currentString.trim()) split.add(currentString.trim())
} }

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.util.inflater
/** /**
* An adapter responsible for showing a list of [Artist] choices in [ArtistPickerDialog]. * An adapter responsible for showing a list of [Artist] choices in [ArtistPickerDialog].
* @param listener A [BasicListListener] for list interactions. * @param listener A [BasicListListener] to bind interactions to.
* @author OxygenCobalt. * @author OxygenCobalt.
*/ */
class ArtistChoiceAdapter(private val listener: BasicListListener) : class ArtistChoiceAdapter(private val listener: BasicListListener) :
@ -45,8 +45,8 @@ class ArtistChoiceAdapter(private val listener: BasicListListener) :
holder.bind(artists[position], listener) holder.bind(artists[position], listener)
/** /**
* Immediately update the tab array. This should be used when initializing the list. * Immediately update the [Artist] choices.
* @param newTabs The new array of tabs to show. * @param newArtists The new [Artist]s to show.
*/ */
fun submitList(newArtists: List<Artist>) { fun submitList(newArtists: List<Artist>) {
if (newArtists != artists) { if (newArtists != artists) {
@ -58,7 +58,7 @@ class ArtistChoiceAdapter(private val listener: BasicListListener) :
/** /**
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical * A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical
* [Artist] item, for use with [ArtistChoiceAdapter]. Use [new] to instantiate a new instance. * [Artist] item, for use with [ArtistChoiceAdapter]. Use [new] to create an instance.
*/ */
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
DialogRecyclerView.ViewHolder(binding.root) { DialogRecyclerView.ViewHolder(binding.root) {

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.collectImmediately
*/ */
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), BasicListListener { abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), BasicListListener {
protected val pickerModel: PickerViewModel by viewModels() protected val pickerModel: PickerViewModel by viewModels()
// Okay to leak this since the Listener will not be called until after full initialization. // Okay to leak this since the Listener will not be called until after initialization.
private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this) private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this)
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
@ -53,7 +53,7 @@ abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerB
collectImmediately(pickerModel.currentArtists) { artists -> collectImmediately(pickerModel.currentArtists) { artists ->
if (!artists.isNullOrEmpty()) { if (!artists.isNullOrEmpty()) {
// Make sure the artist choices align with the current music library. // Make sure the artist choices align with any changes in the music library.
// TODO: I really don't think it makes sense to do this. I'd imagine it would // TODO: I really don't think it makes sense to do this. I'd imagine it would
// be more productive to just exit this dialog rather than try to update it. // be more productive to just exit this dialog rather than try to update it.
artistAdapter.submitList(artists) artistAdapter.submitList(artists)

View file

@ -27,7 +27,7 @@ import org.oxycblt.auxio.util.inflater
/** /**
* [RecyclerView.Adapter] that manages a list of [Directory] instances. * [RecyclerView.Adapter] that manages a list of [Directory] instances.
* @param listener [Listener] for list interactions. * @param listener A [DirectoryAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter<MusicDirViewHolder>() { class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter<MusicDirViewHolder>() {
@ -78,23 +78,34 @@ class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter<Mu
notifyItemRemoved(idx) notifyItemRemoved(idx)
} }
/** /** A Listener for [DirectoryAdapter] interactions. */
* A Listener for [DirectoryAdapter] interactions.
*/
interface Listener { interface Listener {
fun onRemoveDirectory(dir: Directory) fun onRemoveDirectory(dir: Directory)
} }
} }
/** The viewholder for [DirectoryAdapter]. Not intended for use in other adapters. */ /**
* A [RecyclerView.Recycler] that displays a [Directory]. Use [new] to create an instance.
* @author Alexander Capehart (OxygenCobalt)
*/
class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) : class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) :
DialogRecyclerView.ViewHolder(binding.root) { DialogRecyclerView.ViewHolder(binding.root) {
fun bind(item: Directory, listener: DirectoryAdapter.Listener) { /**
binding.dirPath.text = item.resolveName(binding.context) * Bind new data to this instance.
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(item) } * @param dir The new [Directory] to bind.
* @param listener A [DirectoryAdapter.Listener] to bind interactions to.
*/
fun bind(dir: Directory, listener: DirectoryAdapter.Listener) {
binding.dirPath.text = dir.resolveName(binding.context)
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(dir) }
} }
companion object { companion object {
/**
* Create a new instance.
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
fun new(parent: View) = fun new(parent: View) =
MusicDirViewHolder(ItemMusicDirBinding.inflate(parent.context.inflater)) MusicDirViewHolder(ItemMusicDirBinding.inflate(parent.context.inflater))
} }

View file

@ -1,18 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.storage

View file

@ -59,8 +59,7 @@ class MusicDirsDialog :
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
val dirs = settings.getMusicDirs(storageManager) val dirs = settings.getMusicDirs(storageManager)
val newDirs = val newDirs =
MusicDirectories( MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
dirs = dirAdapter.dirs, shouldInclude = isUiModeInclude(requireBinding()))
if (dirs != newDirs) { if (dirs != newDirs) {
logD("Committing changes") logD("Committing changes")
settings.setMusicDirs(newDirs) settings.setMusicDirs(newDirs)
@ -70,7 +69,7 @@ class MusicDirsDialog :
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
val launcher = val launcher =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath) registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs)
// Now that the dialog exists, we get the view manually when the dialog is shown // Now that the dialog exists, we get the view manually when the dialog is shown
// and override its click listener so that the dialog does not auto-dismiss when we // and override its click listener so that the dialog does not auto-dismiss when we
@ -78,7 +77,6 @@ class MusicDirsDialog :
// and the app from crashing in the latter. // and the app from crashing in the latter.
requireDialog().setOnShowListener { requireDialog().setOnShowListener {
val dialog = it as AlertDialog val dialog = it as AlertDialog
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener {
logD("Opening launcher") logD("Opening launcher")
launcher.launch(null) launcher.launch(null)
@ -94,7 +92,6 @@ class MusicDirsDialog :
if (savedInstanceState != null) { if (savedInstanceState != null) {
val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS) val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS)
if (pendingDirs != null) { if (pendingDirs != null) {
dirs = dirs =
MusicDirectories( MusicDirectories(
@ -136,14 +133,26 @@ class MusicDirsDialog :
requireBinding().dirsEmpty.isVisible = dirAdapter.dirs.isEmpty() requireBinding().dirsEmpty.isVisible = dirAdapter.dirs.isEmpty()
} }
private fun addDocTreePath(uri: Uri?) { /**
* Add a Document Tree [Uri] chosen by the user to the current [MusicDirectories] instance.
* @param uri The document tree [Uri] to add, chosen by the user. Will do nothing if the [Uri]
* is null or not valid.
*/
private fun addDocumentTreeUriToDirs(uri: Uri?) {
if (uri == null) { if (uri == null) {
// A null URI means that the user left the file picker without picking a directory // A null URI means that the user left the file picker without picking a directory
logD("No URI given (user closed the dialog)") logD("No URI given (user closed the dialog)")
return return
} }
val dir = parseExcludedUri(uri) // Convert the document tree URI into it's relative path form, which can then be
// parsed into a Directory instance.
val docUri =
DocumentsContract.buildDocumentUriUsingTree(
uri, DocumentsContract.getTreeDocumentId(uri))
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
val dir = Directory.fromDocumentTreeUri(storageManager, treeUri)
if (dir != null) { if (dir != null) {
dirAdapter.add(dir) dirAdapter.add(dir)
requireBinding().dirsEmpty.isVisible = false requireBinding().dirsEmpty.isVisible = false
@ -152,19 +161,6 @@ class MusicDirsDialog :
} }
} }
private fun parseExcludedUri(uri: Uri): Directory? {
// Turn the raw URI into a document tree URI
val docUri =
DocumentsContract.buildDocumentUriUsingTree(
uri, DocumentsContract.getTreeDocumentId(uri))
// Turn it into a semi-usable path
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
// Parsing handles the rest
return Directory.fromDocumentTreeUri(storageManager, treeUri)
}
private fun updateMode() { private fun updateMode() {
val binding = requireBinding() val binding = requireBinding()
if (isUiModeInclude(binding)) { if (isUiModeInclude(binding)) {
@ -174,6 +170,9 @@ class MusicDirsDialog :
} }
} }
/**
* Get if the UI has currently configured [MusicDirectories.shouldInclude] to be true.
*/
private fun isUiModeInclude(binding: DialogMusicDirsBinding) = private fun isUiModeInclude(binding: DialogMusicDirsBinding) =
binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include

View file

@ -48,9 +48,8 @@ val Context.contentResolverSafe: ContentResolver
* arguments should be filled in are represented with a "?". * arguments should be filled in are represented with a "?".
* @param args The arguments used for the selector. * @param args The arguments used for the selector.
* @return A [Cursor] of the queried values, organized by the column projection. * @return A [Cursor] of the queried values, organized by the column projection.
* @throws IllegalStateException If the [ContentResolver] did not successfully return * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
* @see ContentResolver.query * @see ContentResolver.query
* a queried [Cursor].
*/ */
fun ContentResolver.safeQuery( fun ContentResolver.safeQuery(
uri: Uri, uri: Uri,
@ -71,9 +70,8 @@ fun ContentResolver.safeQuery(
* @param args The arguments used for the selector. * @param args The arguments used for the selector.
* @param block The block of code to run with the queried [Cursor]. Will not be ran if the * @param block The block of code to run with the queried [Cursor]. Will not be ran if the
* [Cursor] is empty. * [Cursor] is empty.
* @throws IllegalStateException If the [ContentResolver] did not successfully return * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
* @see ContentResolver.query * @see ContentResolver.query
* a queried [Cursor].
*/ */
inline fun <reified R> ContentResolver.useQuery( inline fun <reified R> ContentResolver.useQuery(
uri: Uri, uri: Uri,

View file

@ -56,9 +56,7 @@ class Indexer private constructor() {
private var controller: Controller? = null private var controller: Controller? = null
private var callback: Callback? = null private var callback: Callback? = null
/** /** Whether music loading is occurring or not. */
* Whether this instance is currently loading music.
*/
val isIndexing: Boolean val isIndexing: Boolean
get() = indexingState != null get() = indexingState != null
@ -226,6 +224,7 @@ class Indexer private constructor() {
} else { } else {
WriteOnlyCacheExtractor(context) WriteOnlyCacheExtractor(context)
} }
val mediaStoreExtractor = val mediaStoreExtractor =
when { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
@ -234,7 +233,9 @@ class Indexer private constructor() {
Api29MediaStoreExtractor(context, cacheDatabase) Api29MediaStoreExtractor(context, cacheDatabase)
else -> Api21MediaStoreExtractor(context, cacheDatabase) else -> Api21MediaStoreExtractor(context, cacheDatabase)
} }
val metadataExtractor = MetadataExtractor(context, mediaStoreExtractor) val metadataExtractor = MetadataExtractor(context, mediaStoreExtractor)
val songs = buildSongs(metadataExtractor, Settings(context)) val songs = buildSongs(metadataExtractor, Settings(context))
if (songs.isEmpty()) { if (songs.isEmpty()) {
// No songs, nothing else to do. // No songs, nothing else to do.
@ -248,6 +249,7 @@ class Indexer private constructor() {
val artists = buildArtists(songs, albums) val artists = buildArtists(songs, albums)
val genres = buildGenres(songs) val genres = buildGenres(songs)
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms") logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
return MusicStore.Library(songs, albums, artists, genres) return MusicStore.Library(songs, albums, artists, genres)
} }
@ -265,11 +267,10 @@ class Indexer private constructor() {
): List<Song> { ): List<Song> {
logD("Starting indexing process") logD("Starting indexing process")
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
// Start initializing the extractors. Here, we will signal that we are loading music, // Start initializing the extractors. Use an indeterminate state, as there is no ETA on
// but have no ETA on how far we are. // how long a media database query will take.
emitIndexing(Indexing.Indeterminate) emitIndexing(Indexing.Indeterminate)
val total = metadataExtractor.init() val total = metadataExtractor.init()
// Handle if we were canceled while initializing the extractors.
yield() yield()
// Note: We use a set here so we can eliminate song duplicates. // Note: We use a set here so we can eliminate song duplicates.
@ -278,19 +279,20 @@ class Indexer private constructor() {
metadataExtractor.parse { rawSong -> metadataExtractor.parse { rawSong ->
songs.add(Song(rawSong, settings)) songs.add(Song(rawSong, settings))
rawSongs.add(rawSong) rawSongs.add(rawSong)
// Handle if we were cancelled while loading a song.
yield()
// Now we can signal a defined progress by showing how many songs we have // Now we can signal a defined progress by showing how many songs we have
// loaded, and the projected amount of songs we found in the library // loaded, and the projected amount of songs we found in the library
// (obtained by the extractors) // (obtained by the extractors)
yield()
emitIndexing(Indexing.Songs(songs.size, total)) emitIndexing(Indexing.Songs(songs.size, total))
} }
// Finalize the extractors with the songs we have no loaded. There is no ETA // Finalize the extractors with the songs we have now loaded. There is no ETA
// on this process, so go back to an indeterminate state. // on this process, so go back to an indeterminate state.
emitIndexing(Indexing.Indeterminate) emitIndexing(Indexing.Indeterminate)
metadataExtractor.finalize(rawSongs) metadataExtractor.finalize(rawSongs)
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms") logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
// Ensure that sorting order is consistent so that grouping is also consistent. // Ensure that sorting order is consistent so that grouping is also consistent.
// Rolling this into the set is not an option, as songs with the same sort result // Rolling this into the set is not an option, as songs with the same sort result
// would be lost. // would be lost.
@ -330,6 +332,7 @@ class Indexer private constructor() {
// Add every raw artist credited to each Song/Album to the grouping. This way, // Add every raw artist credited to each Song/Album to the grouping. This way,
// different multi-artist combinations are not treated as different artists. // different multi-artist combinations are not treated as different artists.
val musicByArtist = mutableMapOf<Artist.Raw, MutableList<Music>>() val musicByArtist = mutableMapOf<Artist.Raw, MutableList<Music>>()
for (song in songs) { for (song in songs) {
for (rawArtist in song._rawArtists) { for (rawArtist in song._rawArtists) {
musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(song) musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(song)
@ -396,8 +399,6 @@ class Indexer private constructor() {
* process. * process.
*/ */
private suspend fun emitCompletion(response: Response) { private suspend fun emitCompletion(response: Response) {
// Handle if this co-routine was canceled in the period between the last loading state
// and this completion state.
yield() yield()
// Swap to the Main thread so that downstream callbacks don't crash from being on // Swap to the Main thread so that downstream callbacks don't crash from being on
// a background thread. Does not occur in emitIndexing due to efficiency reasons. // a background thread. Does not occur in emitIndexing due to efficiency reasons.
@ -415,9 +416,7 @@ class Indexer private constructor() {
} }
} }
/** /** Represents the current state of [Indexer]. */
* Represents the current state of the music loading process.
*/
sealed class State { sealed class State {
/** /**
* Music loading is ongoing. * Music loading is ongoing.
@ -435,7 +434,7 @@ class Indexer private constructor() {
} }
/** /**
* The current progress of the music loader. Usually encapsulated in a [State]. * Represents the current progress of the music loader. Usually encapsulated in a [State].
* @see State.Indexing * @see State.Indexing
*/ */
sealed class Indexing { sealed class Indexing {
@ -453,9 +452,7 @@ class Indexer private constructor() {
class Songs(val current: Int, val total: Int) : Indexing() class Songs(val current: Int, val total: Int) : Indexing()
} }
/** /** Represents the possible outcomes of the music loading process. */
* The possible outcomes of the music loading process.
*/
sealed class Response { sealed class Response {
/** /**
* Music load was successful and produced a [MusicStore.Library]. * Music load was successful and produced a [MusicStore.Library].
@ -469,14 +466,10 @@ class Indexer private constructor() {
*/ */
data class Err(val throwable: Throwable) : Response() data class Err(val throwable: Throwable) : Response()
/** /** Music loading occurred, but resulted in no music. */
* Music loading occurred, but resulted in no music.
*/
object NoMusic : Response() object NoMusic : Response()
/** /** Music loading could not occur due to a lack of storage permissions. */
* Music loading could not occur due to a lack of storage permissions.
*/
object NoPerms : Response() object NoPerms : Response()
} }

View file

@ -109,9 +109,7 @@ class ObservingNotification(context: Context) : ServiceNotification(context, IND
get() = IntegerTable.INDEXER_NOTIFICATION_CODE get() = IntegerTable.INDEXER_NOTIFICATION_CODE
} }
/** /** Notification channel shared by [IndexingNotification] and [ObservingNotification]. */
* Shared channel that [IndexingNotification] and [ObservingNotification] post to.
*/
private val INDEXER_CHANNEL = private val INDEXER_CHANNEL =
ServiceNotification.ChannelInfo( ServiceNotification.ChannelInfo(
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer) id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer)

View file

@ -164,11 +164,16 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
// --- INTERNAL --- // --- INTERNAL ---
/**
* Update the current state to "Active", in which the service signals that music
* loading is on-going.
* @param state The current music loading state.
*/
private fun updateActiveSession(state: Indexer.Indexing) { private fun updateActiveSession(state: Indexer.Indexing) {
// When loading, we want to enter the foreground state so that android does // When loading, we want to enter the foreground state so that android does
// not shut off the loading process. Note that while we will always post the // not shut off the loading process. Note that while we will always post the
// notification when initially starting, we will not update the notification // notification when initially starting, we will not update the notification
// unless it indicates that we have changed it. // unless it indicates that it has changed.
val changed = indexingNotification.updateIndexingState(state) val changed = indexingNotification.updateIndexingState(state)
if (!foregroundManager.tryStartForeground(indexingNotification) && changed) { if (!foregroundManager.tryStartForeground(indexingNotification) && changed) {
logD("Notification changed, re-posting notification") logD("Notification changed, re-posting notification")
@ -178,6 +183,10 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
wakeLock.acquireSafe() wakeLock.acquireSafe()
} }
/**
* Update the current state to "Idle", in which it either does nothing or signals
* that it's currently monitoring the music library for changes.
*/
private fun updateIdleSession() { private fun updateIdleSession() {
if (settings.shouldBeObserving) { if (settings.shouldBeObserving) {
// There are a few reasons why we stay in the foreground with automatic rescanning: // There are a few reasons why we stay in the foreground with automatic rescanning:
@ -199,6 +208,9 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
wakeLock.releaseSafe() wakeLock.releaseSafe()
} }
/**
* Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency.
*/
private fun PowerManager.WakeLock.acquireSafe() { private fun PowerManager.WakeLock.acquireSafe() {
// Avoid unnecessary acquire calls. // Avoid unnecessary acquire calls.
if (!wakeLock.isHeld) { if (!wakeLock.isHeld) {
@ -210,6 +222,9 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
} }
} }
/**
* Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency.
*/
private fun PowerManager.WakeLock.releaseSafe() { private fun PowerManager.WakeLock.releaseSafe() {
// Avoid unnecessary release calls. // Avoid unnecessary release calls.
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
@ -277,16 +292,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
} }
companion object { companion object {
/**
* The amount of time to hold the wake lock when loading music, in milliseconds.
* Equivalent to one minute.
*/
private const val WAKELOCK_TIMEOUT_MS = 60 * 1000L private const val WAKELOCK_TIMEOUT_MS = 60 * 1000L
/**
* The amount of time to wait between a change in the music library and to start
* the music loading process, in milliseconds. Equivalent to half a second.
*/
private const val REINDEX_DELAY_MS = 500L private const val REINDEX_DELAY_MS = 500L
} }
} }