all: remove superfluous comments

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

View file

@ -27,7 +27,7 @@ import coil.ImageLoaderFactory
import coil.request.CachePolicy
import 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()

View file

@ -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
}

View file

@ -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)

View file

@ -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<Artist>?) {
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
}

View file

@ -175,10 +175,7 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), AlbumDetailAd
}
}
/**
* Update the current item selection.
* @param selected The list of selected items.
*/
private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelectedItems(selected)
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)

View file

@ -179,10 +179,6 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
}
}
/**
* Update the currently displayed [Artist]
* @param artist The new [Artist] to display. Null if there is no longer one.
*/
private fun updateItem(artist: Artist?) {
if (artist == null) {
// Artist we were showing no longer exists.
@ -193,17 +189,11 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), DetailAdapte
}
}
/**
* Update the current item selection.
* @param selected The list of selected items.
*/
private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelectedItems(selected)
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)

View file

@ -61,8 +61,7 @@ class DetailViewModel(application: Application) :
private val _currentSong = MutableStateFlow<DetailSong?>(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<DetailSong?>
@ -71,23 +70,16 @@ class DetailViewModel(application: Application) :
// --- ALBUM ---
private val _currentAlbum = MutableStateFlow<Album?>(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<Album?>
get() = _currentAlbum
private val _albumData = MutableStateFlow(listOf<Item>())
/**
* The current list data derived from [currentAlbum], for use in the [Album] detail view.
*/
private val _albumList = MutableStateFlow(listOf<Item>())
/** The current list data derived from [currentAlbum]. */
val albumList: StateFlow<List<Item>>
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<Artist?>(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<Artist?>
get() = _currentArtist
private val _artistData = MutableStateFlow(listOf<Item>())
/**
* The current list derived from [currentArtist], for use in the [Artist] detail view.
*/
val artistList: StateFlow<List<Item>> = _artistData
private val _artistList = MutableStateFlow(listOf<Item>())
/** The current list derived from [currentArtist]. */
val artistList: StateFlow<List<Item>> = _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<Genre?>(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<Genre?>
get() = _currentGenre
private val _genreData = MutableStateFlow(listOf<Item>())
/**
* The current list data derived from [currentGenre], for use in the [Genre] detail view.
*/
val genreList: StateFlow<List<Item>> = _genreData
private val _genreList = MutableStateFlow(listOf<Item>())
/** The current list data derived from [currentGenre]. */
val genreList: StateFlow<List<Item>> = _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<Genre>(uid).also { refreshGenreList(it) }
}
/**
* A wrapper around [MusicStore.Library.find] that asserts that the returned data should
* be valid.
* @param T The type of music that should be found
* @param uid The [Music.UID] of the [T] to find
* @return A [T] with the given [Music.UID]
* @throws IllegalStateException If nothing can be found
*/
private fun <T: Music> requireMusic(uid: Music.UID): T =
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<Item>(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<Item>(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<Item>(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
}
/**

View file

@ -174,10 +174,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
}
}
/**
* Update the currently displayed [Genre]
* @param genre The new [Genre] to display. Null if there is no longer one.
*/
private fun updateItem(genre: Genre?) {
if (genre == null) {
// Genre we were showing no longer exists.
@ -188,12 +184,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), 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<FragmentDetailBinding>(), DetailAdapter
}
}
/**
* Update the current item selection.
* @param selected The list of selected items.
*/
private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelectedItems(selected)
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)

View file

@ -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
}

View file

@ -56,10 +56,6 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
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<DialogSongDetailBinding>() {
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<DialogSongDetailBinding>() {
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

View file

@ -40,7 +40,7 @@ import org.oxycblt.auxio.util.inflater
/**
* An [DetailAdapter] implementing the header and sub-items for the [Album] detail view.
* @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) {

View file

@ -39,7 +39,7 @@ import org.oxycblt.auxio.util.inflater
/**
* A [DetailAdapter] implementing the header and sub-items for the [Artist] detail view.
* @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) {

View file

@ -35,13 +35,13 @@ import org.oxycblt.auxio.util.inflater
/**
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
* @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<Item>
) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), 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.
/**

View file

@ -37,7 +37,7 @@ import org.oxycblt.auxio.util.inflater
/**
* An [DetailAdapter] implementing the header and sub-items for the [Genre] detail view.
* @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) {

View file

@ -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<Song>, 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<Music>) {
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].

View file

@ -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<MusicMode> = getVisibleTabModes()
var currentTabModes: List<MusicMode> = 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<Tab.Visible>().map { it.mode }
}

View file

@ -132,11 +132,6 @@ class AlbumListFragment : ListFragment<FragmentHomeListBinding>(), 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<FragmentHomeListBinding>(), 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<AlbumViewHolder>() {

View file

@ -107,11 +107,6 @@ class ArtistListFragment : ListFragment<FragmentHomeListBinding>(), 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<FragmentHomeListBinding>(), 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<ArtistViewHolder>() {

View file

@ -106,11 +106,6 @@ class GenreListFragment : ListFragment<FragmentHomeListBinding>(), 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<FragmentHomeListBinding>(), 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<GenreViewHolder>() {

View file

@ -142,11 +142,6 @@ class SongListFragment : ListFragment<FragmentHomeListBinding>(), 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<FragmentHomeListBinding>(), 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<SongViewHolder>() {

View file

@ -54,20 +54,17 @@ sealed class Tab(open val mode: MusicMode) {
// Where V is a bit representing the visibility and T is a 3-bit integer representing the
// 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) {

View file

@ -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<TabViewHolder>() {
/**
* 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<Tab>()
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<TabViewH
}
/**
* Swap two tabs with eachother.
* Swap two tabs with each other.
* @param a The position of the first tab to swap.
* @param b The position of the second tab to swap.
*/
@ -97,6 +75,22 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
notifyItemMoved(a, b)
}
/** A listener for interactions specific to tab configuration. */
interface Listener {
/**
* Called when a tab is clicked, requesting that the visibility should be inverted
* (i.e Visible -> Invisible and vice versa).
* @param tabMode The [MusicMode] of the tab clicked.
*/
fun onToggleVisibility(tabMode: MusicMode)
/**
* Called when the drag handle is pressed, requesting that a drag should be started.
* @param viewHolder The [RecyclerView.ViewHolder] to start dragging.
*/
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
}
companion object {
private val PAYLOAD_TAB_CHANGED = Any()
}

View file

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

View file

@ -39,14 +39,10 @@ import org.oxycblt.auxio.music.Song
* @author Alexander Capehart (OxygenCobalt)
*/
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
}

View file

@ -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;
/**

View file

@ -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()

View file

@ -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)
/**

View file

@ -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<Song> {
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<Album> {
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<Artist> {
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<Genre> {
override fun create(data: Genre, options: Options, imageLoader: ImageLoader) =
GenreImageFetcher(options.context, options.size, data)

View file

@ -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) {

View file

@ -44,9 +44,7 @@ object Images {
}
}
// Use whatever size coil gives us to create the mosaic, rounding it to even so that we
// 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

View file

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

View file

@ -112,7 +112,7 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Extende
* closed when the view is destroyed. If a menu is already opened, this call is ignored.
* @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<VB : ViewBinding> : SelectionFragment<VB>(), 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<VB : ViewBinding> : SelectionFragment<VB>(), 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<VB : ViewBinding> : SelectionFragment<VB>(), Extende
}
}
/**
* Internally create a menu for a [Music] item.
* @param anchor The [View] to anchor the menu to.
* @param menuRes The resource of the menu to load.
* @param onMenuItemClick A callback for when a [MenuItem] is selected.
*/
private fun openMusicMenuImpl(
anchor: View,
@MenuRes menuRes: Int,

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -28,24 +28,10 @@ import org.oxycblt.auxio.util.logW
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
/**
* A [RecyclerView.ViewHolder] that can display a playing indicator.
*/
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
/**
* Update the playing indicator within this [RecyclerView.ViewHolder].
* @param isActive True if this item is playing, false otherwise.
* @param isPlaying True if playback is ongoing, false if paused. If this
* is true, [isActive] will also be true.
*/
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
}
// There are actually two states for this adapter:
// 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<VH : RecyclerView.ViewHolder> : RecyclerV
}
}
/**
* A [RecyclerView.ViewHolder] that can display a playing indicator.
*/
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
/**
* Update the playing indicator within this [RecyclerView.ViewHolder].
* @param isActive True if this item is playing, false otherwise.
* @param isPlaying True if playback is ongoing, false if paused. If this
* is true, [isActive] will also be true.
*/
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
}
companion object {
private val PAYLOAD_PLAYING_INDICATOR_CHANGED = Any()
}

View file

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

View file

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

View file

@ -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) :

View file

@ -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

View file

@ -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<String> = listOf(),
/**
* @see Artist.Raw.musicBrainzId
*/
/** @see Artist.Raw.musicBrainzId */
var artistMusicBrainzIds: List<String> = listOf(),
/**
* @see Artist.Raw.name
*/
/** @see Artist.Raw.name */
var artistNames: List<String> = listOf(),
/**
* @see Artist.Raw.sortName
*/
/** @see Artist.Raw.sortName */
var artistSortNames: List<String> = listOf(),
/**
* @see Artist.Raw.musicBrainzId
*/
/** @see Artist.Raw.musicBrainzId */
var albumArtistMusicBrainzIds: List<String> = listOf(),
/**
* @see Artist.Raw.name
*/
/** @see Artist.Raw.name */
var albumArtistNames: List<String> = listOf(),
/**
* @see Artist.Raw.sortName
*/
/** @see Artist.Raw.sortName */
var albumArtistSortNames: List<String> = listOf(),
/**
* @see Genre.Raw
*/
/** @see Genre.Raw.name */
var genreNames: List<String> = listOf()
)
}
@ -669,23 +602,24 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : 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<Song>) : 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<Song>) : 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<Artist.Raw>
) {
// Albums are grouped as follows:
// - If we have a MusicBrainz ID, only group by it. This allows different Albums with the
// same name to be differentiated, which is common in large libraries.
// - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase
// artist name. This allows for case-insensitive artist/album grouping, which can be common
// for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein").
// Cache the hash-code for HashMap efficiency.
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<Music>) : MusicP
* thus included in this list.
*/
val albums: List<Album>
/**
* 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<Music>) : 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<Song>) :
*/
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<Int>) : Comparable<Date>
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 {
/**

View file

@ -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;
/**

View file

@ -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>(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.

View file

@ -31,17 +31,11 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
private val indexer = Indexer.getInstance()
private val _indexerState = MutableStateFlow<Indexer.State?>(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<Indexer.State?> = _indexerState
private val _statistics = MutableStateFlow<Statistics?>(null)
/**
* Statistics about the last completed music load.
* @see Statistics
*/
/** [Statistics] about the last completed music load. */
val statistics: StateFlow<Statistics?>
get() = _statistics
@ -68,16 +62,12 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
}
}
/**
* Requests that the music library should be re-loaded while leveraging the cache.
*/
/** Requests that the music library should be re-loaded while leveraging the cache. */
fun refresh() {
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)
}

View file

@ -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<List<Artist>> = 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<Song> = BasicComparator()
/**
* A shared instance configured for [Album]s that can be re-used.
*/
/** A re-usable instance configured for [Album]s. */
val ALBUM: Comparator<Album> = BasicComparator()
/**
* A shared instance configured for [Artist]s that can be re-used.
*/
/** A re-usable instance configured for [Artist]s. */
val ARTIST: Comparator<Artist> = BasicComparator()
/**
* A shared instance configured for [Genre]s that can be re-used.
*/
/** A re-usable instance configured for [Genre]s. */
val GENRE: Comparator<Genre> = 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<Int>()
/**
* A shared instance configured for [Long]s that can be re-used.
*/
/** A re-usable instance configured for [Long]s. */
val LONG = NullableComparator<Long>()
/**
* A shared instance configured for [Date]s that can be re-used.
*/
/** A re-usable instance configured for [Date]s. */
val DATE = NullableComparator<Date>()
}
}

View file

@ -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

View file

@ -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<String>()
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<Song.Raw>) {
// 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

View file

@ -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<Song.Raw>) = 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

View file

@ -110,7 +110,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String>
}
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())
}

View file

@ -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<Artist>) {
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) {

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.collectImmediately
*/
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), 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<DialogMusicPickerB
collectImmediately(pickerModel.currentArtists) { artists ->
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)

View file

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

View file

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

View file

@ -59,8 +59,7 @@ class MusicDirsDialog :
.setPositiveButton(R.string.lbl_save) { _, _ ->
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

View file

@ -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 <reified R> ContentResolver.useQuery(
uri: Uri,

View file

@ -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<Song> {
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<Artist.Raw, MutableList<Music>>()
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()
}

View file

@ -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)

View file

@ -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
}
}