diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt index 4fe121e41..202494d77 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt @@ -27,7 +27,7 @@ import coil.ImageLoaderFactory import coil.request.CachePolicy import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher 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.MusicKeyer import org.oxycblt.auxio.settings.Settings @@ -68,7 +68,7 @@ class AuxioApp : Application(), ImageLoaderFactory { add(GenreImageFetcher.Factory()) } // Use our own crossfade with error drawable support - .transitionFactory(ErrorCrossfadeTransitionFractory()) + .transitionFactory(ErrorCrossfadeTransitionFactory()) // Not downloading anything, so no disk-caching .diskCachePolicy(CachePolicy.DISABLED) .build() diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index 94986e412..65b921d82 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -17,144 +17,102 @@ 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 { /** SongViewHolder */ const val VIEW_TYPE_SONG = 0xA000 - /** AlbumViewHolder */ const val VIEW_TYPE_ALBUM = 0xA001 - /** ArtistViewHolder */ const val VIEW_TYPE_ARTIST = 0xA002 - /** GenreViewHolder */ const val VIEW_TYPE_GENRE = 0xA003 - /** HeaderViewHolder */ const val VIEW_TYPE_HEADER = 0xA004 - /** SortHeaderViewHolder */ const val VIEW_TYPE_SORT_HEADER = 0xA005 - /** AlbumDetailViewHolder */ const val VIEW_TYPE_ALBUM_DETAIL = 0xA006 - /** AlbumSongViewHolder */ const val VIEW_TYPE_ALBUM_SONG = 0xA007 - /** ArtistDetailViewHolder */ const val VIEW_TYPE_ARTIST_DETAIL = 0xA008 - /** ArtistAlbumViewHolder */ const val VIEW_TYPE_ARTIST_ALBUM = 0xA009 - /** ArtistSongViewHolder */ const val VIEW_TYPE_ARTIST_SONG = 0xA00A - /** GenreDetailViewHolder */ const val VIEW_TYPE_GENRE_DETAIL = 0xA00B - /** DiscHeaderViewHolder */ const val VIEW_TYPE_DISC_HEADER = 0xA00C - /** "Music playback" notification code */ const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0 - /** "Music loading" notification code */ const val INDEXER_NOTIFICATION_CODE = 0xA0A1 - - /** Intent request code */ + /** MainActivity Intent request code */ const val REQUEST_CODE = 0xA0C0 - /** RepeatMode.NONE */ const val REPEAT_MODE_NONE = 0xA100 - /** RepeatMode.ALL */ const val REPEAT_MODE_ALL = 0xA101 - /** RepeatMode.TRACK */ const val REPEAT_MODE_TRACK = 0xA102 - /** PlaybackMode.IN_ARTIST */ const val PLAYBACK_MODE_IN_ARTIST = 0xA104 - /** PlaybackMode.IN_ALBUM */ const val PLAYBACK_MODE_IN_ALBUM = 0xA105 - /** PlaybackMode.ALL_SONGS */ const val PLAYBACK_MODE_ALL_SONGS = 0xA106 - /** DisplayMode.NONE (No Longer used but still reserved) */ // const val DISPLAY_MODE_NONE = 0xA107 /** MusicMode._GENRES */ const val MUSIC_MODE_GENRES = 0xA108 - /** MusicMode._ARTISTS */ const val MUSIC_MODE_ARTISTS = 0xA109 - /** MusicMode._ALBUMS */ const val MUSIC_MODE_ALBUMS = 0xA10A - - /** MusicMode._SONGS */ + /** MusicMode.SONGS */ 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 */ const val SORT_BY_NAME = 0xA10C - /** Sort.ByArtist */ const val SORT_BY_ARTIST = 0xA10D - /** Sort.ByAlbum */ const val SORT_BY_ALBUM = 0xA10E - /** Sort.ByYear */ const val SORT_BY_YEAR = 0xA10F - /** Sort.ByDuration */ const val SORT_BY_DURATION = 0xA114 - /** Sort.ByCount */ const val SORT_BY_COUNT = 0xA115 - /** Sort.ByDisc */ const val SORT_BY_DISC = 0xA116 - /** Sort.ByTrack */ const val SORT_BY_TRACK = 0xA117 - /** Sort.ByDateAdded */ const val SORT_BY_DATE_ADDED = 0xA118 - /** ReplayGainMode.Off (No longer used but still reserved) */ // const val REPLAY_GAIN_MODE_OFF = 0xA110 /** ReplayGainMode.Track */ const val REPLAY_GAIN_MODE_TRACK = 0xA111 - /** ReplayGainMode.Album */ const val REPLAY_GAIN_MODE_ALBUM = 0xA112 - /** ReplayGainMode.Dynamic */ const val REPLAY_GAIN_MODE_DYNAMIC = 0xA113 - /** ActionMode.Next */ const val ACTION_MODE_NEXT = 0xA119 - /** ActionMode.Repeat */ const val ACTION_MODE_REPEAT = 0xA11A - /** ActionMode.Shuffle */ const val ACTION_MODE_SHUFFLE = 0xA11B - /** CoverMode.Off */ const val COVER_MODE_OFF = 0xA11C - /** CoverMode.MediaStore */ const val COVER_MODE_MEDIA_STORE = 0xA11D - /** CoverMode.Quality */ const val COVER_MODE_QUALITY = 0xA11E } diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index a32897ba3..f7503eabe 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.systemBarInsetsCompat /** - * The single [AppCompatActivity] for Auxio. + * Auxio's single [AppCompatActivity]. * * TODO: Add error screens * @@ -85,6 +85,14 @@ class MainActivity : AppCompatActivity() { 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 { if (intent == null) { return false @@ -97,7 +105,6 @@ class MainActivity : AppCompatActivity() { // RestoreState action. return true } - intent.putExtra(KEY_INTENT_USED, true) val action = @@ -106,19 +113,16 @@ class MainActivity : AppCompatActivity() { AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll else -> return false } - playbackModel.startAction(action) - return true } private fun setupTheme() { val settings = Settings(this) - + // Set up the current theme. AppCompatDelegate.setDefaultNightMode(settings.theme) - - // The black theme has a completely separate set of styles since style attributes cannot - // be modified at runtime. + // Set up the color scheme. Note that the black theme has it's own set + // of styles since the color schemes cannot be modified at runtime. if (isNight && settings.useBlackTheme) { logD("Applying black theme [accent ${settings.accent}]") setTheme(settings.accent.blackTheme) @@ -130,7 +134,6 @@ class MainActivity : AppCompatActivity() { private fun setupEdgeToEdge(contentView: View) { WindowCompat.setDecorFitsSystemWindows(window, false) - contentView.setOnApplyWindowInsetsListener { view, insets -> val bars = insets.systemBarInsetsCompat view.updatePadding(left = bars.left, right = bars.right) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 318d36dfc..7e2c51e96 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -121,7 +121,7 @@ class MainFragment : collect(navModel.exploreNavigationItem, ::handleExploreNavigation) collect(navModel.exploreNavigationArtists, ::handleExplorePicker) collectImmediately(playbackModel.song, ::updateSong) - collect(playbackModel.artistPlaybackPickerSong, ::handlePlaybackPicker) + collect(playbackModel.artistPlaybackPickerSong, ::handlePlaybackArtistPicker) } override fun onStart() { @@ -216,22 +216,25 @@ class MainFragment : if (playbackModel.song.value == null) { // Sometimes lingering drags can un-hide the playback sheet even when we intend to // 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 // every frame. - callback.updateEnabledState() + callback.invalidateEnabled() return true } private fun handleMainNavigation(action: MainNavigationAction?) { - if (action == null) return + if (action == null) { + // Nothing to do. + return + } when (action) { - is MainNavigationAction.Expand -> tryExpandAll() - is MainNavigationAction.Collapse -> tryCollapseAll() + is MainNavigationAction.Expand -> tryExpandSheets() + is MainNavigationAction.Collapse -> tryCollapseSheets() // TODO: Figure out how to clear out the selections as one moves between screens. is MainNavigationAction.Directions -> findNavController().navigate(action.directions) } @@ -241,12 +244,13 @@ class MainFragment : private fun handleExploreNavigation(item: Music?) { if (item != null) { - tryCollapseAll() + tryCollapseSheets() } } private fun handleExplorePicker(items: List?) { if (items != null) { + // Navigate to the analogous artist picker dialog. navModel.mainNavigateTo( MainNavigationAction.Directions( MainFragmentDirections.actionPickNavigationArtist( @@ -257,14 +261,15 @@ class MainFragment : private fun updateSong(song: Song?) { if (song != null) { - tryUnhideAll() + tryShowSheets() } else { - tryHideAll() + tryHideAllSheets() } } - private fun handlePlaybackPicker(song: Song?) { + private fun handlePlaybackArtistPicker(song: Song?) { if (song != null) { + // Navigate to the analogous artist picker dialog. navModel.mainNavigateTo( MainNavigationAction.Directions( MainFragmentDirections.actionPickPlaybackArtist(song.uid))) @@ -272,18 +277,18 @@ class MainFragment : } } - private fun tryExpandAll() { + private fun tryExpandSheets() { val binding = requireBinding() val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior 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 } } - private fun tryCollapseAll() { + private fun tryCollapseSheets() { val binding = requireBinding() val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior @@ -292,13 +297,12 @@ class MainFragment : // Make sure the queue is also collapsed here. val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? - playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED queueSheetBehavior?.state = NeoBottomSheetBehavior.STATE_COLLAPSED } } - private fun tryUnhideAll() { + private fun tryShowSheets() { val binding = requireBinding() val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior @@ -318,7 +322,7 @@ class MainFragment : } } - private fun tryHideAll() { + private fun tryHideAllSheets() { val binding = requireBinding() val playbackSheetBehavior = 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 - * fragments and the playback panel. + * A [OnBackPressedCallback] that overrides the back button to first navigate out of + * internal app components, such as the Bottom Sheets or Explore Navigation. */ private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) { override fun handleOnBackPressed() { @@ -353,36 +357,45 @@ class MainFragment : val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? + // If expanded, collapse the queue sheet first. if (queueSheetBehavior != null && queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED && playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) { - // Collapse the queue first if it is expanded. queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED return } + // If expanded, collapse the playback sheet next. if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED && playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) { - // Then collapse the playback sheet. playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED return } + // Then try to navigate out of the explore navigation fragments (i.e Detail Views) 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 playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? - val exploreNavController = binding.exploreNavHost.findNavController() isEnabled = + queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED || playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED || - queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED || exploreNavController.currentDestination?.id != exploreNavController.graph.startDestinationId } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 9a71c9489..82f14fefb 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -175,10 +175,7 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd 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?) { if (album == null) { // Album we were showing no longer exists. @@ -189,12 +186,6 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd 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) { if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) { detailAdapter.setPlayingItem(song, isPlaying) @@ -204,10 +195,6 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd } } - /** - * Handle a navigation event. - * @param item The [Music] to navigate to, null if there is no item. - */ private fun handleNavigation(item: Music?) { val binding = requireBinding() when (item) { @@ -216,7 +203,7 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd is Song -> { if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) { logD("Navigating to a song in this album") - scrollToItem(item) + scrollToAlbumSong(item) navModel.finishExploreNavigation() } else { logD("Navigating to another album") @@ -250,11 +237,7 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd } } - /** - * 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) { + private fun scrollToAlbumSong(song: Song) { // Calculate where the item for the currently played song is val pos = detailModel.albumList.value.indexOf(song) @@ -293,10 +276,6 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd } } - /** - * Update the current item selection. - * @param selected The list of selected items. - */ private fun updateSelection(selected: List) { detailAdapter.setSelectedItems(selected) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index c793768cb..f9af946bb 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -179,10 +179,6 @@ class ArtistDetailFragment : ListFragment(), 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?) { if (artist == null) { // Artist we were showing no longer exists. @@ -193,17 +189,11 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte 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) { val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value) val playingItem = when (parent) { - // Always highlight a playing album from this artist. + // Always highlight a playing album if it's from this artist. is Album -> parent // If the parent is the artist itself, use the currently playing song. currentArtist -> song @@ -214,10 +204,6 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte 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?) { val binding = requireBinding() @@ -253,10 +239,6 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte } } - /** - * Update the current item selection. - * @param selected The list of selected items. - */ private fun updateSelection(selected: List) { detailAdapter.setSelectedItems(selected) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 30c1e3a96..a25d8f7b0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -61,8 +61,7 @@ class DetailViewModel(application: Application) : private val _currentSong = MutableStateFlow(null) /** - * The current [Song] that should be displayed in the [Song] detail view. Null if there - * is no [Song]. + * The current [DetailSong] to display. Null if there is nothing to show. * TODO: De-couple Song and Properties? */ val currentSong: StateFlow @@ -71,23 +70,16 @@ class DetailViewModel(application: Application) : // --- ALBUM --- private val _currentAlbum = MutableStateFlow(null) - /** - * The current [Album] that should be displayed in the [Album] detail view. Null if there - * is no [Album]. - */ + /** The current [Album] to display. Null if there is nothing to show. */ val currentAlbum: StateFlow get() = _currentAlbum - private val _albumData = MutableStateFlow(listOf()) - /** - * The current list data derived from [currentAlbum], for use in the [Album] detail view. - */ + private val _albumList = MutableStateFlow(listOf()) + /** The current list data derived from [currentAlbum]. */ val albumList: StateFlow> - get() = _albumData + get() = _albumList - /** - * The current [Sort] used for [Song]s in the [Album] detail view. - */ + /** The current [Sort] used for [Song]s in [albumList]. */ var albumSort: Sort get() = settings.detailAlbumSort set(value) { @@ -99,22 +91,15 @@ class DetailViewModel(application: Application) : // --- ARTIST --- private val _currentArtist = MutableStateFlow(null) - /** - * The current [Artist] that should be displayed in the [Artist] detail view. Null if there - * is no [Artist]. - */ + /** The current [Artist] to display. Null if there is nothing to show. */ val currentArtist: StateFlow get() = _currentArtist - private val _artistData = MutableStateFlow(listOf()) - /** - * The current list derived from [currentArtist], for use in the [Artist] detail view. - */ - val artistList: StateFlow> = _artistData + private val _artistList = MutableStateFlow(listOf()) + /** The current list derived from [currentArtist]. */ + val artistList: StateFlow> = _artistList - /** - * The current [Sort] used for [Song]s in the [Artist] detail view. - */ + /** The current [Sort] used for [Song]s in [artistList]. */ var artistSort: Sort get() = settings.detailArtistSort set(value) { @@ -126,22 +111,15 @@ class DetailViewModel(application: Application) : // --- GENRE --- private val _currentGenre = MutableStateFlow(null) - /** - * The current [Genre] that should be displayed in the [Genre] detail view. Null if there - * is no [Genre]. - */ + /** The current [Genre] to display. Null if there is nothing to show. */ val currentGenre: StateFlow get() = _currentGenre - private val _genreData = MutableStateFlow(listOf()) - /** - * The current list data derived from [currentGenre], for use in the [Genre] detail view. - */ - val genreList: StateFlow> = _genreData + private val _genreList = MutableStateFlow(listOf()) + /** The current list data derived from [currentGenre]. */ + val genreList: StateFlow> = _genreList - /** - * The current [Sort] used for [Song]s in the [Genre] detail view. - */ + /** The current [Sort] used for [Song]s in [genreList]. */ var genreSort: Sort get() = settings.detailGenreSort set(value) { @@ -254,14 +232,6 @@ class DetailViewModel(application: Application) : _currentGenre.value = requireMusic(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 requireMusic(uid: Music.UID): T = requireNotNull(unlikelyToBeNull(musicStore.library).find(uid)) { "Invalid id provided" } @@ -275,20 +245,13 @@ class DetailViewModel(application: Application) : _currentSong.value = DetailSong(song, null) currentSongJob = viewModelScope.launch(Dispatchers.IO) { - val info = loadSongProperties(song) + val info = loadProperties(song) yield() _currentSong.value = DetailSong(song, info) } } - /** - * 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 { + private fun loadProperties(song: Song): DetailSong.Properties { // 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 // 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) } - /** - * 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) { logD("Refreshing album data") val data = mutableListOf(album) @@ -374,13 +333,9 @@ class DetailViewModel(application: Application) : 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) { logD("Refreshing artist data") val data = mutableListOf(artist) @@ -421,13 +376,9 @@ class DetailViewModel(application: Application) : 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) { logD("Refreshing genre data") val data = mutableListOf(genre) @@ -436,7 +387,7 @@ class DetailViewModel(application: Application) : data.addAll(genre.artists) data.add(SortHeader(R.string.lbl_songs)) data.addAll(genreSort.songs(genre.songs)) - _genreData.value = data + _genreList.value = data } /** diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 863af252d..62fa4f937 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -174,10 +174,6 @@ class GenreDetailFragment : ListFragment(), 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?) { if (genre == null) { // Genre we were showing no longer exists. @@ -188,12 +184,6 @@ class GenreDetailFragment : ListFragment(), DetailAdapter 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) { var item: Item? = null @@ -208,10 +198,6 @@ class GenreDetailFragment : ListFragment(), DetailAdapter 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?) { when (item) { is Song -> { @@ -236,10 +222,6 @@ class GenreDetailFragment : ListFragment(), DetailAdapter } } - /** - * Update the current item selection. - * @param selected The list of selected items. - */ private fun updateSelection(selected: List) { detailAdapter.setSelectedItems(selected) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt index 692d7428e..ea9f53c42 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt @@ -39,7 +39,6 @@ constructor( attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.editTextStyle ) : TextInputEditText(context, attrs, defStyleAttr) { - init { // Enable selection, but still disable focus (i.e Keyboard opening) setTextIsSelectable(true) @@ -50,10 +49,8 @@ constructor( // Make text immutable override fun getFreezesText() = false - // Prevent editing by default override fun getDefaultEditable() = false - // Remove the movement method that allows cursor scrolling override fun getDefaultMovementMethod() = null } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index b07cee3ed..e93f22894 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -56,10 +56,6 @@ class SongDetailDialog : ViewBindingDialogFragment() { 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?) { val binding = requireBinding() @@ -71,19 +67,16 @@ class SongDetailDialog : ViewBindingDialogFragment() { if (song.properties != null) { // Finished loading Song properties, populate and show the list of Song information. + binding.detailLoading.isInvisible = true + binding.detailContainer.isInvisible = false + val context = requireContext() - // File name binding.detailFileName.setText(song.song.path.name) - // Relative (Parent) directory binding.detailRelativeDir.setText(song.song.path.parent.resolveName(context)) - // Format binding.detailFormat.setText(song.properties.resolvedMimeType.resolveName(context)) - // Size binding.detailSize.setText(Formatter.formatFileSize(context, song.song.size)) - // Duration binding.detailDuration.setText(song.song.durationMs.formatDurationMs(true)) - // Bit rate (if present) if (song.properties.bitrateKbps != null) { binding.detailBitrate.setText( getString(R.string.fmt_bitrate, song.properties.bitrateKbps)) @@ -91,16 +84,12 @@ class SongDetailDialog : ViewBindingDialogFragment() { binding.detailBitrate.setText(R.string.def_bitrate) } - // Sample rate (if present) if (song.properties.sampleRateHz != null) { binding.detailSampleRate.setText( getString(R.string.fmt_sample_rate, song.properties.sampleRateHz)) } else { binding.detailSampleRate.setText(R.string.def_sample_rate) } - - binding.detailLoading.isInvisible = true - binding.detailContainer.isInvisible = false } else { // Loading is still on-going, don't show anything yet. binding.detailLoading.isInvisible = false diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index 790994ad3..e0da21c55 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -40,7 +40,7 @@ import org.oxycblt.auxio.util.inflater /** * 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) */ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt index 285e2bc8e..75e11ee26 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt @@ -39,7 +39,7 @@ import org.oxycblt.auxio.util.inflater /** * 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) */ class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index da563ffdb..03af7f30f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -35,13 +35,13 @@ import org.oxycblt.auxio.util.inflater /** * 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 * internal list. * @author Alexander Capehart (OxygenCobalt) */ abstract class DetailAdapter( - private val callback: Listener, + private val listener: Listener, itemCallback: DiffUtil.ItemCallback ) : SelectionIndicatorAdapter(), AuxioRecyclerView.SpanSizeLookup { // 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) { when (val item = differ.currentList[position]) { 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) } - /** - * An extended [ExtendedListListener] for [DetailAdapter] implementations. - */ + /** An extended [ExtendedListListener] for [DetailAdapter] implementations. */ interface Listener : ExtendedListListener { // TODO: Split off into sub-listeners if a collapsing toolbar is implemented. /** diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt index 13211d150..cedf7af0a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.util.inflater /** * 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) */ class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index c0d5e0f6f..0ad6e5c36 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -93,7 +93,7 @@ class HomeFragment : // our transitions. val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_AXIS, -1) if (axis > -1) { - initAxisTransitions(axis) + setupAxisTransitions(axis) } } } @@ -199,7 +199,7 @@ class HomeFragment : // Handle main actions (Search, Settings, About) R.id.action_search -> { logD("Navigating to search") - initAxisTransitions(MaterialSharedAxis.Z) + setupAxisTransitions(MaterialSharedAxis.Z) findNavController().navigate(HomeFragmentDirections.actionShowSearch()) } R.id.action_settings -> { @@ -238,10 +238,6 @@ class HomeFragment : 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) { binding.homePager.adapter = HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner) @@ -264,10 +260,6 @@ class HomeFragment : 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) { // Update the sort options to align with those allowed by the tab val isVisible: (Int) -> Boolean = when (tabMode) { @@ -310,10 +302,6 @@ class HomeFragment : 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) { if (!recreate) { // Nothing to do @@ -328,11 +316,6 @@ class HomeFragment : 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?) { val binding = requireBinding() 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) { if (response is Indexer.Response.Ok) { 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) { // Remove all content except for the progress indicator. 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, isFastScrolling: Boolean) { val binding = requireBinding() // 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?) { val action = when (item) { @@ -462,14 +426,10 @@ class HomeFragment : else -> return } - initAxisTransitions(MaterialSharedAxis.X) + setupAxisTransitions(MaterialSharedAxis.X) findNavController().navigate(action) } - /** - * Update the current item selection. - * @param selected The list of selected items. - */ private fun updateSelection(selected: List) { val binding = requireBinding() if (binding.homeSelectionToolbar.updateSelectionAmount(selected.size) && @@ -481,24 +441,7 @@ class HomeFragment : } } - /** - * 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) { + private fun setupAxisTransitions(axis: Int) { // Sanity check to avoid in-correct axis transitions check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) { "Not expecting Y axis transition" @@ -510,9 +453,23 @@ class HomeFragment : 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. - * @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 lifecycleOwner The [LifecycleOwner], whose Lifecycle is required by * [FragmentStateAdapter]. diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 03936b5a9..eb353778c 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -77,7 +77,7 @@ class HomeViewModel(application: Application) : * A list of [MusicMode] corresponding to the current [Tab] configuration, excluding * invisible [Tab]s. */ - var currentTabModes: List = getVisibleTabModes() + var currentTabModes: List = makeTabModes() private set private val _currentTabMode = MutableStateFlow(currentTabModes[0]) @@ -133,7 +133,7 @@ class HomeViewModel(application: Application) : when (key) { context.getString(R.string.set_key_lib_tabs) -> { // Tabs changed, update the current tabs and set up a re-create event. - currentTabModes = getVisibleTabModes() + currentTabModes = makeTabModes() _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() { _shouldRecreate.value = false @@ -211,9 +212,10 @@ class HomeViewModel(application: Application) : } /** - * Get the [MusicMode]s of the visible [Tab]s from the [Tab] configuration. - * @return A list of [MusicMode]s for each visible [Tab] in the [Tab] configuration. + * Create a list of [MusicMode]s representing a simpler version of 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().map { it.mode } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index febc48ebc..fcc6e4220 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -132,11 +132,6 @@ class AlbumListFragment : ListFragment(), FastScrollRec 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) { // If an album is playing, highlight it within this adapter. albumAdapter.setPlayingItem(parent as? Album, isPlaying) @@ -144,7 +139,7 @@ class AlbumListFragment : ListFragment(), FastScrollRec /** * 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) : SelectionIndicatorAdapter() { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 22910817e..5be7b68b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -107,11 +107,6 @@ class ArtistListFragment : ListFragment(), FastScrollRe 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) { // If an artist is playing, highlight it within this adapter. homeAdapter.setPlayingItem(parent as? Artist, isPlaying) @@ -119,7 +114,7 @@ class ArtistListFragment : ListFragment(), FastScrollRe /** * 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) : SelectionIndicatorAdapter() { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 134494dda..14b4a557d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -106,11 +106,6 @@ class GenreListFragment : ListFragment(), FastScrollRec 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) { // If a genre is playing, highlight it within this adapter. homeAdapter.setPlayingItem(parent as? Genre, isPlaying) @@ -118,7 +113,7 @@ class GenreListFragment : ListFragment(), FastScrollRec /** * 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) : SelectionIndicatorAdapter() { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 7e03dd878..58cbb7bdc 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -142,11 +142,6 @@ class SongListFragment : ListFragment(), FastScrollRecy 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) { if (parent == null) { homeAdapter.setPlayingItem(song, isPlaying) @@ -158,7 +153,7 @@ class SongListFragment : ListFragment(), FastScrollRecy /** * 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) : SelectionIndicatorAdapter() { diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index 0d0b8f2fb..f22f68f8e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -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 // 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 /** * 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 - /** - * Maps between the integer code in the tab sequence and the actual [MusicMode] instance. - */ + /** Maps between the integer code in the tab sequence and it's [MusicMode]. */ private val MODE_TABLE = arrayOf(MusicMode.SONGS, MusicMode.ALBUMS, MusicMode.ARTISTS, MusicMode.GENRES) @@ -82,7 +79,6 @@ sealed class Tab(open val mode: MusicMode) { var sequence = 0b0100 var shift = SEQUENCE_LEN * 4 - for (tab in distinct) { val bin = when (tab) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index 9627123f2..6e73764ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -33,34 +33,12 @@ import org.oxycblt.auxio.util.inflater * @param listener A [Listener] for tab interactions. */ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter() { - /** - * 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. - */ + /** The current array of [Tab]s. */ var tabs = arrayOf() private set override fun getItemCount() = tabs.size - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.new(parent) - override fun onBindViewHolder(holder: TabViewHolder, position: Int) { holder.bind(tabs[position], listener) } @@ -86,7 +64,7 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter 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 { private val PAYLOAD_TAB_CHANGED = Any() } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index 103824091..62730833e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -109,6 +109,6 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd } companion object { - const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS" + private const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS" } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt index e950577a1..76aaaf39f 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt @@ -39,14 +39,10 @@ import org.oxycblt.auxio.music.Song * @author Alexander Capehart (OxygenCobalt) */ 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) - /** - * The target that will recieve the requested [Bitmap]. - */ + /** The target that will receive the requested [Bitmap]. */ interface Target { /** * Configure the [ImageRequest.Builder] to enable [Target]-specific configuration. @@ -65,11 +61,7 @@ class BitmapProvider(private val context: Context) { } 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 handleLock = Any() /** If this provider is currently attempting to load something. */ val isBusy: Boolean @@ -82,13 +74,12 @@ class BitmapProvider(private val context: Context) { */ @Synchronized fun load(song: Song, target: Target) { - // Increment the handle, indicating a newer request being created. - val handle = synchronized(handleLock) { ++currentHandle } - // Be even safer and cancel the previous request. + // Increment the handle, indicating a newer request has been created + val handle = ++currentHandle currentRequest?.run { disposable.dispose() } currentRequest = null - val request = + val imageRequest = target.onConfigRequest( ImageRequest.Builder(context) .data(song) @@ -99,31 +90,32 @@ class BitmapProvider(private val context: Context) { // callback. .target( onSuccess = { - synchronized(handleLock) { + synchronized(this) { 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()) } } }, onError = { - synchronized(handleLock) { + synchronized(this) { 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) } } }) - 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 - * stray loading callbacks. + * Release this instance, cancelling any currently running operations. */ @Synchronized fun release() { - synchronized(handleLock) { ++currentHandle } + ++currentHandle currentRequest?.run { disposable.dispose() } currentRequest = null } diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt index d90910b5a..35354c51a 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt @@ -24,17 +24,11 @@ import org.oxycblt.auxio.IntegerTable * @author Alexander Capehart (OxygenCobalt) */ enum class CoverMode { - /** - * Do not load album covers ("Off"). - */ + /** Do not load album covers ("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, - /** - * Load high-quality covers directly from music files ("Quality"). - */ + /** Load high-quality covers directly from music files ("Quality"). */ QUALITY; /** diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt index 1ac3d953f..388ec7c0f 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -65,8 +65,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private val cornerRadius: Float init { - // Android wants you to make separate attributes for each view type, but will - // then throw an error if you do because of duplicate attribute names. + // Obtain some StyledImageView attributes to use later when theming the cusotm view. @SuppressLint("CustomViewStyleable") 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 @@ -123,7 +122,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr override fun onAttachedToWindow() { super.onAttachedToWindow() // Initialize each component before this view is drawn. - invalidateAlpha() + invalidateImageAlpha() invalidatePlayingIndicator() invalidateSelectionIndicator() } @@ -135,13 +134,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr override fun setEnabled(enabled: Boolean) { super.setEnabled(enabled) - invalidateAlpha() + invalidateImageAlpha() invalidatePlayingIndicator() } override fun setSelected(selected: Boolean) { super.setSelected(selected) - invalidateAlpha() + invalidateImageAlpha() invalidatePlayingIndicator() } @@ -185,18 +184,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr playbackIndicatorView.isPlaying = value } - /** - * Invalidate the overall opacity of this view. - */ - private fun invalidateAlpha() { + private fun invalidateImageAlpha() { // 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. alpha = if (isSelected || isEnabled) 1f else 0.5f } - /** - * Invalidate the view's playing ([isSelected]) indicator. - */ private fun invalidatePlayingIndicator() { if (isSelected) { // 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() { // Set up a target transition for the selection indicator. val targetAlpha: Float val targetDuration: Long if (isActivated) { - // Activated -> Show selection indicator + // View is "activated" (i.e marked as selected), so show the selection indicator. targetAlpha = 1f targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong() } else { - // Activated -> Hide selection indicator. + // View is not "activated", hide the selection indicator. targetAlpha = 0f targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong() diff --git a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt index 900c49a3c..7760961c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt @@ -45,18 +45,13 @@ class PlaybackIndicatorView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr) { - // The playing drawable will cycle through an active equalizer animation. private val playingIndicatorDrawable = 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 = context.getDrawableCompat(R.drawable.ic_paused_indicator_24) - - // Required transformation matrices for the drawables. private val indicatorMatrix = Matrix() private val indicatorMatrixSrc = RectF() private val indicatorMatrixDst = RectF() - private val settings = Settings(context) /** diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index 18fc6ee51..50c202046 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -66,17 +66,13 @@ private constructor(private val context: Context, private val album: Album) : Fe 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 { override fun create(data: Song, options: Options, imageLoader: ImageLoader) = 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 { override fun create(data: Album, options: Options, imageLoader: ImageLoader) = AlbumCoverFetcher(options.context, data) @@ -100,9 +96,7 @@ private constructor( return Images.createMosaic(context, results, size) } - /** - * [Fetcher.Factory] implementation. - */ + /** [Fetcher.Factory] implementation. */ class Factory : Fetcher.Factory { override fun create(data: Artist, options: Options, imageLoader: ImageLoader) = ArtistImageFetcher(options.context, options.size, data) @@ -124,6 +118,7 @@ private constructor( return Images.createMosaic(context, results, size) } + /** [Fetcher.Factory] implementation. */ class Factory : Fetcher.Factory { override fun create(data: Genre, options: Options, imageLoader: ImageLoader) = GenreImageFetcher(options.context, options.size, data) diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFractory.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFactory.kt similarity index 89% rename from app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFractory.kt rename to app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFactory.kt index ac7ff0700..676cc53bb 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFractory.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFactory.kt @@ -26,11 +26,10 @@ import coil.transition.Transition import coil.transition.TransitionTarget /** - * A copy of [CrossfadeTransition.Factory] that applies a transition to error results. You know. - * Like they used to. + * A copy of [CrossfadeTransition.Factory] that also applies a transition to error results. * @author Coil Team, Alexander Capehart (OxygenCobalt) */ -class ErrorCrossfadeTransitionFractory : Transition.Factory { +class ErrorCrossfadeTransitionFactory : Transition.Factory { override fun create(target: TransitionTarget, result: ImageResult): Transition { // Don't animate if the request was fulfilled by the memory cache. if (result is SuccessResult && result.dataSource == DataSource.MEMORY_CACHE) { diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt index 97cd143f0..543ebd55c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt @@ -44,9 +44,7 @@ object Images { } } - // Use whatever size coil gives us to create the mosaic, rounding it to even so that we - // get a symmetrical mosaic [and to prevent bugs]. If there is no size, default to a - // 512x512 mosaic. + // Use whatever size coil gives us to create the mosaic. val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize()) val mosaicFrameSize = Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2)) @@ -65,16 +63,13 @@ object Images { break } - // Run the bitmap through a transform to make sure it's a square of the desired - // resolution. + // Run the bitmap through a transform to reflect the configuration of other images. val bitmap = SquareFrameTransform.INSTANCE.transform( BitmapFactory.decodeStream(stream), mosaicFrameSize) - canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null) x += bitmap.width - if (x == mosaicSize.width) { x = 0 y += bitmap.height @@ -90,6 +85,11 @@ object Images { 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 { val size = pxOrElse { 512 } return if (size.mod(2) > 0) size + 1 else size diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt index 8487f9246..1e31a237e 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt @@ -50,9 +50,7 @@ class SquareFrameTransform : Transformation { } companion object { - /** - * A shared instance that can be re-used. - */ + /** A re-usable instance. */ val INSTANCE = SquareFrameTransform() } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 9c0740b14..ab2230231 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -112,7 +112,7 @@ abstract class ListFragment : SelectionFragment(), Extende * 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 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) { logD("Launching new album menu: ${album.rawName}") @@ -148,7 +148,7 @@ abstract class ListFragment : SelectionFragment(), Extende * 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 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) { logD("Launching new artist menu: ${artist.rawName}") @@ -181,7 +181,7 @@ abstract class ListFragment : SelectionFragment(), Extende * 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 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) { logD("Launching new genre menu: ${genre.rawName}") @@ -209,12 +209,6 @@ abstract class ListFragment : SelectionFragment(), 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( anchor: View, @MenuRes menuRes: Int, diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt index 52adb7c5a..e265ce0b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt @@ -38,19 +38,6 @@ open class AuxioRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : 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 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 + } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt index f71e08917..644f29b2f 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt @@ -40,19 +40,6 @@ class DialogRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : 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 bottomDivider = MaterialDivider(context) private val spacingMedium = context.getDimenPixels(R.dimen.spacing_medium) @@ -90,10 +77,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr invalidateDividers() } - /** - * Measure a [divider] with the equivalent of match_parent and wrap_content. - * @param divider The divider to measure. - */ private fun measureDivider(divider: MaterialDivider) { val widthMeasureSpec = ViewGroup.getChildMeasureSpec( @@ -108,9 +91,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr divider.measure(widthMeasureSpec, heightMeasureSpec) } - /** - * Invalidate the visibility of both dividers. - */ private fun invalidateDividers() { val lmm = layoutManager as LinearLayoutManager // 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 = 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) + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt index 4195dbfaf..f5f24a6f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt @@ -28,24 +28,10 @@ import org.oxycblt.auxio.util.logW * @author Alexander Capehart (OxygenCobalt) */ abstract class PlayingIndicatorAdapter : RecyclerView.Adapter() { - /** - * 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: - // This is sub-divided into two states: // - 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 - // displaying + // marked as "playing" or not. private var currentItem: Item? = null private var isPlaying = false @@ -119,6 +105,19 @@ abstract class PlayingIndicatorAdapter : 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 { private val PAYLOAD_PLAYING_INDICATOR_CHANGED = Any() } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt index 450ad64c4..a9c565c1f 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt @@ -28,17 +28,6 @@ import org.oxycblt.auxio.music.Music */ abstract class SelectionIndicatorAdapter : PlayingIndicatorAdapter() { - /** - * 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() override fun onBindViewHolder(holder: VH, position: Int, payloads: List) { @@ -79,6 +68,17 @@ abstract class SelectionIndicatorAdapter : } } + /** + * 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 { private val PAYLOAD_SELECTION_INDICATOR_CHANGED = Any() } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt index b666fe052..ef9842501 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt @@ -113,6 +113,7 @@ class SyncListDiffer( /** * Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only * use it if the changes are trivial. + * @param newList The list to update to. */ fun submitList(newList: List) { if (newList == currentList) { @@ -126,6 +127,7 @@ class SyncListDiffer( /** * 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. + * @param newList The list to update to. */ fun replaceList(newList: List) { if (newList == currentList) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index 68a2e0d27..4282a1b6b 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -35,7 +35,7 @@ import org.oxycblt.auxio.util.getPlural 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) */ 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) */ 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) */ 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) */ 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) */ class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) : diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt index 8a9500296..88b72f8f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt @@ -38,9 +38,7 @@ class SelectionToolbarOverlay @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { - // This will be populated after the inflation completes. private lateinit var innerToolbar: MaterialToolbar - // The selection toolbar will be overlaid over the inner toolbar when shown. private val selectionToolbar = MaterialToolbar(context).apply { setNavigationIcon(R.drawable.ic_close_24) @@ -50,7 +48,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr isInvisible = true } } - // Animator to handle selection visibility animations private var fadeThroughAnimator: ValueAnimator? = null override fun onFinishInflate() { @@ -61,7 +58,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } // The inner toolbar should be the first child. innerToolbar = getChildAt(0) as MaterialToolbar - // Now layer the selection toolbar on top. + // Selection toolbar should appear on top of the inner toolbar. 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 * pressed. * @param listener The OnClickListener to respond to this interaction. + * @see MaterialToolbar.setNavigationOnClickListener */ fun setOnSelectionCancelListener(listener: OnClickListener) { 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 * [MaterialToolbar]. * @param listener The [OnMenuItemClickListener] to respond to this interaction. + * @see MaterialToolbar.setOnMenuItemClickListener */ fun setOnMenuItemClickListener(listener: OnMenuItemClickListener?) { selectionToolbar.setOnMenuItemClickListener(listener) @@ -134,7 +133,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (!isLaidOut) { // Not laid out, just change it immediately while are not shown to the user. // This is an initialization, so we return false despite changing. - changeToolbarAlpha(targetInnerAlpha) + setToolbarsAlpha(targetInnerAlpha) return false } @@ -146,7 +145,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr fadeThroughAnimator = ValueAnimator.ofFloat(innerToolbar.alpha, targetInnerAlpha).apply { duration = targetDuration - addUpdateListener { changeToolbarAlpha(it.animatedValue as Float) } + addUpdateListener { setToolbarsAlpha(it.animatedValue as Float) } 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 * inverse opacity of the selection [MaterialToolbar]. */ - private fun changeToolbarAlpha(innerAlpha: Float) { + private fun setToolbarsAlpha(innerAlpha: Float) { innerToolbar.apply { alpha = innerAlpha isInvisible = innerAlpha == 0f diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index cfe637721..3b0751306 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -167,20 +167,13 @@ sealed class Music : Item { override fun toString() = "${format.namespace}:${mode.intCode.toString(16)}-$uuid" /** - * Defines the format of this [UID]. - * @param namespace The namespace that will be used in the [UID]'s string representation - * to indicate the format. + * Internal marker of [Music.UID] format type. + * @param namespace Namespace to use in the [Music.UID]'s string representation. */ private enum class Format(val namespace: String) { - /** - * Auxio-style [UID]s derived from hash of the*non-subjective, unlikely-to-change - * metadata. - */ + /** @see auxio */ AUXIO("org.oxycblt.auxio"), - - /** - * Auxio-style [UID]s derived from a MusicBrainz ID. - */ + /** @see musicBrainz */ MUSICBRAINZ("org.musicbrainz") } @@ -250,9 +243,7 @@ sealed class Music : Item { } companion object { - /** - * Cached collator instance to be used with [makeCollationKeyImpl]. - */ + /** Cached collator instance re-used with [makeCollationKeyImpl]. */ 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 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 - /** - * 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 - /** - * 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 + /** * 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. */ 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 * favored instead for accessing the audio file. @@ -333,24 +322,20 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { Path( name = requireNotNull(raw.fileName) { "Invalid raw: No display name" }, 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 = MimeType( fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" }, 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" } - /** - * 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" } - /** - * 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" } 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. */ var mediaStoreId: Long? = null, - /** - * @see Song.dateAdded - */ + /** @see Song.dateAdded */ 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, - /** - * @see Song.path - */ + /** @see Song.path */ var fileName: String? = null, - /** - * @see Song.path - */ + /** @see Song.path */ var directory: Directory? = null, - /** - * @see Song.size - */ + /** @see Song.size */ var size: Long? = null, - /** - * @see Song.durationMs - */ + /** @see Song.durationMs */ var durationMs: Long? = null, - /** - * @see Song.mimeType - */ + /** @see Song.mimeType */ var extensionMimeType: String? = null, - /** - * @see Song.mimeType - */ + /** @see Song.mimeType */ var formatMimeType: String? = null, - /** - * @see Music.UID - */ + /** @see Music.UID */ var musicBrainzId: String? = null, - /** - * @see Music.rawName - */ + /** @see Music.rawName */ var name: String? = null, - /** - * @see Music.rawSortName - */ + /** @see Music.rawSortName */ var sortName: String? = null, - /** - * @see Song.track - */ + /** @see Song.track */ var track: Int? = null, - /** - * @see Song.disc - */ + /** @see Song.disc */ var disc: Int? = null, - /** - * @see Song.date - */ + /** @see Song.date */ var date: Date? = null, - /** - * @see Album.Raw.mediaStoreId - */ + /** @see Album.Raw.mediaStoreId */ var albumMediaStoreId: Long? = null, - /** - * @see Album.Raw.musicBrainzId - */ + /** @see Album.Raw.musicBrainzId */ var albumMusicBrainzId: String? = null, - /** - * @see Album.Raw.name - */ + /** @see Album.Raw.name */ var albumName: String? = null, - /** - * @see Album.Raw.sortName - */ + /** @see Album.Raw.sortName */ var albumSortName: String? = null, - /** - * @see Album.Raw.type - */ + /** @see Album.Raw.type */ var albumTypes: List = listOf(), - /** - * @see Artist.Raw.musicBrainzId - */ + /** @see Artist.Raw.musicBrainzId */ var artistMusicBrainzIds: List = listOf(), - /** - * @see Artist.Raw.name - */ + /** @see Artist.Raw.name */ var artistNames: List = listOf(), - /** - * @see Artist.Raw.sortName - */ + /** @see Artist.Raw.sortName */ var artistSortNames: List = listOf(), - /** - * @see Artist.Raw.musicBrainzId - */ + /** @see Artist.Raw.musicBrainzId */ var albumArtistMusicBrainzIds: List = listOf(), - /** - * @see Artist.Raw.name - */ + /** @see Artist.Raw.name */ var albumArtistNames: List = listOf(), - /** - * @see Artist.Raw.sortName - */ + /** @see Artist.Raw.sortName */ var albumArtistSortNames: List = listOf(), - /** - * @see Genre.Raw - */ + /** @see Genre.Raw.name */ var genreNames: List = listOf() ) } @@ -669,23 +602,24 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( * TODO: Date ranges? */ val date: Date? + /** * The [Type] of this album, signifying the type of release it actually is. * Defaults to [Type.Album]. */ + val type = raw.type ?: Type.Album(null) /** * The URI to a MediaStore-provided album cover. These images will be fast to load, but * at the cost of image quality. */ + 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 - /** - * 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 init { @@ -798,9 +732,7 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( */ 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 /** @@ -999,34 +931,28 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( * cover art. */ val mediaStoreId: Long, - /** - * @see Music.uid - */ + /** @see Music.uid */ val musicBrainzId: UUID?, - /** - * @see Music.rawName - */ + /** @see Music.rawName */ val name: String, - /** - * @see Music.rawSortName - */ + /** @see Music.rawSortName */ val sortName: String?, - /** - * @see Album.type - */ + /** @see Album.type */ val type: Type?, - /** - * @see Artist.Raw.name - */ + /** @see Artist.Raw.name */ val rawArtists: List ) { + // Albums are grouped as follows: + // - If we have a MusicBrainz ID, only group by it. This allows different Albums with the + // same name to be differentiated, which is common in large libraries. + // - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase + // artist name. This allows for case-insensitive artist/album grouping, which can be common + // for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein"). + // Cache the hash-code for HashMap efficiency. private val 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 equals(other: Any?): Boolean { @@ -1068,11 +994,13 @@ class Artist constructor(private val raw: Raw, songAlbums: List) : MusicP * thus included in this list. */ val albums: List + /** * The duration of all [Song]s in the artist, in milliseconds. * Will be null if there are no songs. */ val durationMs: Long? + /** * Whether this artist is considered a "collaborator", i.e it is not directly credited on * any [Album]. @@ -1159,19 +1087,19 @@ class Artist constructor(private val raw: Raw, songAlbums: List) : MusicP * **This is only meant for use within the music package.** */ class Raw( - /** - * @see Music.UID - */ + /** @see Music.UID */ val musicBrainzId: UUID? = null, - /** - * @see Music.rawName - */ + /** @see Music.rawName */ val name: String? = null, - /** - * @see Music.rawSortName - */ + /** @see Music.rawSortName */ 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. private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode() @@ -1271,6 +1199,10 @@ class Genre constructor(private val raw: Raw, override val songs: List) : */ 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. private val hashCode = name?.lowercase().hashCode() @@ -1361,22 +1293,16 @@ class Date private constructor(private val tokens: List) : Comparable private fun StringBuilder.appendDate(): StringBuilder { // Construct an ISO-8601 date, dropping precision that doesn't exist. - append(year.toFixedString(4)) - append("-${(month ?: return this).toFixedString(2)}") - append("-${(day ?: return this).toFixedString(2)}") - append("T${(hour ?: return this).toFixedString(2)}") - append(":${(minute ?: return this.append('Z')).toFixedString(2)}") - append(":${(second ?: return this.append('Z')).toFixedString(2)}") + append(year.toStringFixed(4)) + append("-${(month ?: return this).toStringFixed(2)}") + append("-${(day ?: return this).toStringFixed(2)}") + append("T${(hour ?: return this).toStringFixed(2)}") + append(":${(minute ?: return this.append('Z')).toStringFixed(2)}") + append(":${(second ?: return this.append('Z')).toStringFixed(2)}") return this.append('Z') } - /** - * 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) + private fun Int.toStringFixed(len: Int) = toString().padStart(len, '0').substring(0 until len) companion object { /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt index eeb4994db..d2c3802a5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt @@ -24,24 +24,13 @@ import org.oxycblt.auxio.IntegerTable * @author Alexander Capehart (OxygenCobalt) */ enum class MusicMode { - /** - * Configure with respect to [Song] instances. - */ + /** Configure with respect to [Song] instances. */ SONGS, - - /** - * Configure with respect to [Album] instances. - */ + /** Configure with respect to [Album] instances. */ ALBUMS, - - /** - * Configure with respect to [Artist] instances. - */ + /** Configure with respect to [Artist] instances. */ ARTISTS, - - /** - * Configure with respect to [Genre] instances. - */ + /** Configure with respect to [Genre] instances. */ GENRES; /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index 432a37de5..71857bd22 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -20,8 +20,6 @@ package org.oxycblt.auxio.music import android.content.Context import android.net.Uri 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.contentResolverSafe @@ -92,8 +90,8 @@ class MusicStore private constructor() { init { // 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 - // same loop for efficiency. + // Finalize them to ensure they are well-formed. Also initialize the UID map in + // the same loop for efficiency. for (song in songs) { song._finalize() 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]. - * @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. */ fun sanitize(genre: Genre) = find(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 { /** * Called when the current [Library] has changed. diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index 9acb85250..e9684aa22 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -31,17 +31,11 @@ class MusicViewModel : ViewModel(), Indexer.Callback { private val indexer = Indexer.getInstance() private val _indexerState = MutableStateFlow(null) - /** - * The current music loading state, or null if no loading is going on. - * @see Indexer.State - */ + /** The current music loading state, or null if no loading is going on. */ val indexerState: StateFlow = _indexerState private val _statistics = MutableStateFlow(null) - /** - * Statistics about the last completed music load. - * @see Statistics - */ + /** [Statistics] about the last completed music load. */ val statistics: StateFlow 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() { indexer.requestReindex(true) } - /** - * Requests that the music library should be re-loaded while ignoring the cache. - */ + /** Requests that the music library be re-loaded without the cache. */ fun rescan() { indexer.requestReindex(false) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt index 8ab2c073b..878d4f64a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt @@ -129,19 +129,14 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { */ val intCode: Int // 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 - // representation of the sort mode. + // representing if the sort is in ascending or descending order, and M is the + // integer representation of the sort mode. get() = mode.intCode.shl(1) or if (isAscending) 1 else 0 sealed class Mode { - /** - * The integer representation of this sort mode. - */ + /** The integer representation of this sort mode. */ 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 /** @@ -276,9 +271,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { compareBy(BasicComparator.ALBUM)) } - /** - * Sort by the duration of an item. - */ + /** Sort by the duration of an item. */ object ByDuration : Mode() { override val intCode: Int get() = IntegerTable.SORT_BY_DURATION @@ -494,9 +487,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { } companion object { - /** - * A shared instance configured for [Artist]s that can be re-used. - */ + /** A re-usable configured for [Artist]s.. */ val ARTISTS: Comparator> = ListComparator(BasicComparator.ARTIST) } } @@ -520,21 +511,13 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { } companion object { - /** - * A shared instance configured for [Song]s that can be re-used. - */ + /** A re-usable instance configured for [Song]s. */ val SONG: Comparator = BasicComparator() - /** - * A shared instance configured for [Album]s that can be re-used. - */ + /** A re-usable instance configured for [Album]s. */ val ALBUM: Comparator = BasicComparator() - /** - * A shared instance configured for [Artist]s that can be re-used. - */ + /** A re-usable instance configured for [Artist]s. */ val ARTIST: Comparator = BasicComparator() - /** - * A shared instance configured for [Genre]s that can be re-used. - */ + /** A re-usable instance configured for [Genre]s. */ val GENRE: Comparator = BasicComparator() } } @@ -553,17 +536,11 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { } companion object { - /** - * A shared instance configured for [Int]s that can be re-used. - */ + /** A re-usable instance configured for [Int]s. */ val INT = NullableComparator() - /** - * A shared instance configured for [Long]s that can be re-used. - */ + /** A re-usable instance configured for [Long]s. */ val LONG = NullableComparator() - /** - * A shared instance configured for [Date]s that can be re-used. - */ + /** A re-usable instance configured for [Date]s. */ val DATE = NullableComparator() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt index 6477f8fca..55a78ef58 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt @@ -44,7 +44,7 @@ interface CacheExtractor { 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. * @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. */ private object Columns { - /** - * @see Song.Raw.mediaStoreId - */ + /** @see Song.Raw.mediaStoreId */ const val MEDIA_STORE_ID = "msid" - /** - * @see Song.Raw.dateAdded - */ + /** @see Song.Raw.dateAdded */ const val DATE_ADDED = "date_added" - /** - * @see Song.Raw.dateModified - */ + /** @see Song.Raw.dateModified */ const val DATE_MODIFIED = "date_modified" - - /** - * @see Song.Raw.size - */ + /** @see Song.Raw.size */ const val SIZE = "size" - /** - * @see Song.Raw.durationMs - */ + /** @see Song.Raw.durationMs */ const val DURATION = "duration" - /** - * @see Song.Raw.formatMimeType - */ + /** @see Song.Raw.formatMimeType */ const val FORMAT_MIME_TYPE = "fmt_mime" - - /** - * @see Song.Raw.musicBrainzId - */ + /** @see Song.Raw.musicBrainzId */ const val MUSIC_BRAINZ_ID = "mbid" - /** - * @see Song.Raw.name - */ + /** @see Song.Raw.name */ const val NAME = "name" - /** - * @see Song.Raw.sortName - */ + /** @see Song.Raw.sortName */ const val SORT_NAME = "sort_name" - - /** - * @see Song.Raw.track - */ + /** @see Song.Raw.track */ const val TRACK = "track" - /** - * @see Song.Raw.disc - */ + /** @see Song.Raw.disc */ const val DISC = "disc" - /** - * @see [Song.Raw.date - */ + /** @see Song.Raw.date */ const val DATE = "date" - - /** - * @see [Song.Raw.albumMusicBrainzId - */ + /** @see Song.Raw.albumMusicBrainzId */ const val ALBUM_MUSIC_BRAINZ_ID = "album_mbid" - /** - * @see Song.Raw.albumName - */ + /** @see Song.Raw.albumName */ const val ALBUM_NAME = "album" - /** - * @see Song.Raw.albumSortName - */ + /** @see Song.Raw.albumSortName */ const val ALBUM_SORT_NAME = "album_sort" - /** - * @see Song.Raw.albumReleaseTypes - */ + /** @see Song.Raw.albumTypes */ const val ALBUM_TYPES = "album_types" - - /** - * @see Song.Raw.artistMusicBrainzIds - */ + /** @see Song.Raw.artistMusicBrainzIds */ const val ARTIST_MUSIC_BRAINZ_IDS = "artists_mbid" - /** - * @see Song.Raw.artistNames - */ + /** @see Song.Raw.artistNames */ const val ARTIST_NAMES = "artists" - /** - * @see Song.Raw.artistSortNames - */ + /** @see Song.Raw.artistSortNames */ 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" - /** - * @see Song.Raw.albumArtistNames - */ + /** @see Song.Raw.albumArtistNames */ const val ALBUM_ARTIST_NAMES = "album_artists" - /** - * @see Song.Raw.albumArtistSortNames - */ + /** @see Song.Raw.albumArtistSortNames */ const val ALBUM_ARTIST_SORT_NAMES = "album_artists_sort" - - /** - * @see Song.Raw.genreNames - */ + /** @see Song.Raw.genreNames */ const val GENRE_NAMES = "genres" } companion object { - /** - * The file name of the database. - */ - 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" + private const val DB_NAME = "auxio_music_cache.db" + private const val DB_VERSION = 1 + private const val TABLE_RAW_SONGS = "raw_songs" @Volatile private var INSTANCE: CacheDatabase? = null diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt index 7374d4024..bf9e37072 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt @@ -81,14 +81,10 @@ abstract class MediaStoreExtractor( * @return A [Cursor] of the music data returned from the database. */ open fun init(): Cursor { - // Initialize sub-extractors for later use. - cacheExtractor.init() - val start = System.currentTimeMillis() + cacheExtractor.init() val settings = Settings(context) val storageManager = context.getSystemServiceCompat(StorageManager::class) - // Set up the volume list for concrete implementations to use. - volumes = storageManager.storageVolumesCompat val args = mutableListOf() var selector = BASE_SELECTOR @@ -151,8 +147,6 @@ abstract class MediaStoreExtractor( artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.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 // 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 @@ -183,18 +177,21 @@ abstract class MediaStoreExtractor( } } + volumes = storageManager.storageVolumesCompat logD("Finished initialization in ${System.currentTimeMillis() - start}ms") 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) { // Free the cursor (and it's resources) cursor?.close() cursor = null - - // Finalize sub-extractors cacheExtractor.finalize(rawSongs) } @@ -502,6 +499,7 @@ open class Api29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtra override fun init(): Cursor { val cursor = super.init() + // Set up cursor indices for later use. trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) return cursor } @@ -537,6 +535,7 @@ class Api30MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) override fun init(): Cursor { val cursor = super.init() + // Set up cursor indices for later use. trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER) return cursor diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt index 38bdbbdda..9a9797c09 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt @@ -56,8 +56,9 @@ class MetadataExtractor( fun init() = mediaStoreExtractor.init().count /** - * Finalize this extractor with the newly parsed [Song.Raw]. This actually finalizes the - * sub-extractors that this instance relies on. + * 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) = mediaStoreExtractor.finalize(rawSongs) @@ -85,7 +86,6 @@ class MetadataExtractor( spin@ while (true) { for (i in taskPool.indices) { val task = taskPool[i] - if (task != null) { val finishedRaw = task.get() if (finishedRaw != null) { @@ -105,7 +105,6 @@ class MetadataExtractor( // Spin until all of the remaining tasks are complete. for (i in taskPool.indices) { val task = taskPool[i] - if (task != null) { val finishedRaw = task.get() ?: continue@spin emit(finishedRaw) @@ -118,9 +117,6 @@ class MetadataExtractor( } companion object { - /** - * The amount of [Task]s this instance can return - */ private const val TASK_CAPACITY = 8 } } @@ -158,7 +154,6 @@ class Task(context: Context, private val raw: Song.Raw) { logW(e.stackTraceToString()) null } - if (format == null) { logD("Nothing could be extracted for ${raw.name}") return raw diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt index 1c384ab1a..58f9d34b2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt @@ -110,7 +110,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList } 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()) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt index 303fa0bd6..44a4d6fab 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.util.inflater /** * 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. */ class ArtistChoiceAdapter(private val listener: BasicListListener) : @@ -45,8 +45,8 @@ class ArtistChoiceAdapter(private val listener: BasicListListener) : holder.bind(artists[position], listener) /** - * Immediately update the tab array. This should be used when initializing the list. - * @param newTabs The new array of tabs to show. + * Immediately update the [Artist] choices. + * @param newArtists The new [Artist]s to show. */ fun submitList(newArtists: List) { if (newArtists != artists) { @@ -58,7 +58,7 @@ class ArtistChoiceAdapter(private val listener: BasicListListener) : /** * 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) : DialogRecyclerView.ViewHolder(binding.root) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt index 865aad79f..e105964d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt @@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.collectImmediately */ abstract class ArtistPickerDialog : ViewBindingDialogFragment(), BasicListListener { 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) override fun onCreateBinding(inflater: LayoutInflater) = @@ -53,7 +53,7 @@ abstract class ArtistPickerDialog : ViewBindingDialogFragment 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 // be more productive to just exit this dialog rather than try to update it. artistAdapter.submitList(artists) diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt index c66e83cae..d34eb2f91 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.util.inflater /** * [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) */ class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter() { @@ -78,23 +78,34 @@ class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter. - */ - -package org.oxycblt.auxio.music.storage diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt index c0543542b..6b800eb25 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt @@ -59,8 +59,7 @@ class MusicDirsDialog : .setPositiveButton(R.string.lbl_save) { _, _ -> val dirs = settings.getMusicDirs(storageManager) val newDirs = - MusicDirectories( - dirs = dirAdapter.dirs, shouldInclude = isUiModeInclude(requireBinding())) + MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding())) if (dirs != newDirs) { logD("Committing changes") settings.setMusicDirs(newDirs) @@ -70,7 +69,7 @@ class MusicDirsDialog : override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) { 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 // 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. requireDialog().setOnShowListener { val dialog = it as AlertDialog - dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { logD("Opening launcher") launcher.launch(null) @@ -94,7 +92,6 @@ class MusicDirsDialog : if (savedInstanceState != null) { val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS) - if (pendingDirs != null) { dirs = MusicDirectories( @@ -136,14 +133,26 @@ class MusicDirsDialog : 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) { // A null URI means that the user left the file picker without picking a directory logD("No URI given (user closed the dialog)") 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) { dirAdapter.add(dir) 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() { val binding = requireBinding() 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) = binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt index 5aae60d5a..97c398ec3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt @@ -48,9 +48,8 @@ val Context.contentResolverSafe: ContentResolver * arguments should be filled in are represented with a "?". * @param args The arguments used for the selector. * @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 - * a queried [Cursor]. */ fun ContentResolver.safeQuery( uri: Uri, @@ -71,9 +70,8 @@ fun ContentResolver.safeQuery( * @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 * [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 - * a queried [Cursor]. */ inline fun ContentResolver.useQuery( uri: Uri, diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index f8686f096..323a62f4a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -56,9 +56,7 @@ class Indexer private constructor() { private var controller: Controller? = null private var callback: Callback? = null - /** - * Whether this instance is currently loading music. - */ + /** Whether music loading is occurring or not. */ val isIndexing: Boolean get() = indexingState != null @@ -226,6 +224,7 @@ class Indexer private constructor() { } else { WriteOnlyCacheExtractor(context) } + val mediaStoreExtractor = when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> @@ -234,7 +233,9 @@ class Indexer private constructor() { Api29MediaStoreExtractor(context, cacheDatabase) else -> Api21MediaStoreExtractor(context, cacheDatabase) } + val metadataExtractor = MetadataExtractor(context, mediaStoreExtractor) + val songs = buildSongs(metadataExtractor, Settings(context)) if (songs.isEmpty()) { // No songs, nothing else to do. @@ -248,6 +249,7 @@ class Indexer private constructor() { val artists = buildArtists(songs, albums) val genres = buildGenres(songs) logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms") + return MusicStore.Library(songs, albums, artists, genres) } @@ -265,11 +267,10 @@ class Indexer private constructor() { ): List { logD("Starting indexing process") val start = System.currentTimeMillis() - // Start initializing the extractors. Here, we will signal that we are loading music, - // but have no ETA on how far we are. + // Start initializing the extractors. Use an indeterminate state, as there is no ETA on + // how long a media database query will take. emitIndexing(Indexing.Indeterminate) val total = metadataExtractor.init() - // Handle if we were canceled while initializing the extractors. yield() // Note: We use a set here so we can eliminate song duplicates. @@ -278,19 +279,20 @@ class Indexer private constructor() { metadataExtractor.parse { rawSong -> songs.add(Song(rawSong, settings)) 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 // loaded, and the projected amount of songs we found in the library // (obtained by the extractors) + yield() 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. emitIndexing(Indexing.Indeterminate) metadataExtractor.finalize(rawSongs) logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms") + // 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 // 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, // different multi-artist combinations are not treated as different artists. val musicByArtist = mutableMapOf>() + for (song in songs) { for (rawArtist in song._rawArtists) { musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(song) @@ -396,8 +399,6 @@ class Indexer private constructor() { * process. */ 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() // 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. @@ -415,9 +416,7 @@ class Indexer private constructor() { } } - /** - * Represents the current state of the music loading process. - */ + /** Represents the current state of [Indexer]. */ sealed class State { /** * 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 */ sealed class Indexing { @@ -453,9 +452,7 @@ class Indexer private constructor() { class Songs(val current: Int, val total: Int) : Indexing() } - /** - * The possible outcomes of the music loading process. - */ + /** Represents the possible outcomes of the music loading process. */ sealed class Response { /** * Music load was successful and produced a [MusicStore.Library]. @@ -469,14 +466,10 @@ class Indexer private constructor() { */ 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() - /** - * 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() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt index c823effc8..8e56fb7be 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt @@ -109,9 +109,7 @@ class ObservingNotification(context: Context) : ServiceNotification(context, IND get() = IntegerTable.INDEXER_NOTIFICATION_CODE } -/** - * Shared channel that [IndexingNotification] and [ObservingNotification] post to. - */ +/** Notification channel shared by [IndexingNotification] and [ObservingNotification]. */ private val INDEXER_CHANNEL = ServiceNotification.ChannelInfo( id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer) diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index 9a6a4573a..3ee3bd4c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -164,11 +164,16 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { // --- 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) { // 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 // 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) if (!foregroundManager.tryStartForeground(indexingNotification) && changed) { logD("Notification changed, re-posting notification") @@ -178,6 +183,10 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { 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() { if (settings.shouldBeObserving) { // 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() } + /** + * Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency. + */ private fun PowerManager.WakeLock.acquireSafe() { // Avoid unnecessary acquire calls. 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() { // Avoid unnecessary release calls. if (wakeLock.isHeld) { @@ -277,16 +292,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { } 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 - - /** - * 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 } }